15. React i Webpack

Wyzwania:

  • dowiesz się, dlaczego powstał React,
  • skonfigurujesz swój pierwszy projekt oparty o React,
  • dowiesz się, czym są komponenty i jak wygląda składnia JSX,
  • napiszesz pierwszą aplikację w React.js.

15.1. Czym jest React?

Po kilku modułach czystego JavaScriptu i wielu liniach napisanego kodu jesteśmy gotowi, aby przeskoczyć na nieco wyższy poziom!

Za chwilę zaczniesz swoją przygodę z Reactem. W jego opanowaniu pomoże Ci przede wszystkim znajomość JS, ale również koncepcji komponentów, którą poznaliśmy zarówno od strony HTML, SCSS, jak i JS-a.

Ucząc się Reacta, stworzymy prostą aplikację to-do, w której będą znajdować się karteczki z zapisanymi rzeczami do zrobienia. Będą one ułożone w kolumnach, a kolumny – w listach. Dzięki temu będziemy mogli stworzyć tyle list, ile zechcemy.

Zanim jednak przejdziemy do praktyki, krótko wyjaśnimy, czym w ogóle jest React.

Po co powstał React?

Zaczniemy od krótkiej historii internetu, przy czym skupimy się na tym, jak mniej–więcej wyglądało życie frontend developera.

Pierwsze strony internetowe budowano przy użyciu klasycznych technologii — HTML i CSS. Nie było w tych stronach żadnej dynamiki i w zasadzie serwer służył tylko do wysyłania odpowiednich plików do klienta, czyli przeglądarki. Miała ona za zadanie jedynie odczytać te pliki i odpowiednio je wyświetlić w swoim oknie.

Z biegiem czasu pojawiły się strony generowane dynamicznie przez aplikację na serwerze. Przeglądarka nadal tylko wyświetlała kod HTML i CSS, a każda zmiana widoku wymagała przeładowania strony. Dla przykładu, formularz kontaktowy trzeba było wypełnić i wysłać na serwer. Powodowało to wczytanie strony na nowo i dopiero wtedy użytkownik dowiadywał się, czy wszystkie pola formularza zostały poprawnie wypełnione.

Dopiero później powstała nowa technologia – JavaScript! Dzięki niemu możliwe stało się np. sprawdzanie poprawności wypełnienia formularza (czyli walidacja) bezpośrednio w przeglądarce. To otwierało olbrzymi potencjał możliwości, które początkowo były najczęściej wykorzystywane do – popularnych w tamtym czasie – zegarków i ikon podążających za kursorem myszy.

Musiało minąć jeszcze trochę czasu, zanim JS zyskał bardzo ważną funkcjonalność – AJAX. Do dziś dzięki AJAX-owi możliwe jest dynamiczne doładowywanie treści czy wysyłanie formularza bez konieczności przeładowania strony. Była jednak istotną przeszkodą w wykorzystywaniu tej technologii – brak jednolitego standardu JavaScriptu wśród twórców najpopularniejszych przeglądarek. Stąd pojawiały się strony działające wyłącznie w jednej z przeglądarek, ponieważ developerzy rzadko decydowali się na pisanie osobnego kodu JS dla każdej z najpopularniejszych przeglądarek.

Odpowiedzią na ten problem była biblioteka jQuery, która bardzo szybko stała się bardzo popularna. Pozwalała ona developerom nie przejmować się różnicami pomiędzy przeglądarkami – wystarczyło napisać jedną wersję skryptu w oparciu o jQuery, aby kod działał we wszystkich przeglądarkach. To przyczyniło się do szybkiej popularyzacji używania JS-a do manipulowania drzewem DOM, obsługi AJAX-a, walidacji i wielu innych funkcjonalności.

Biblioteka jQuery była bardzo dobrym rozwiązaniem do momentu, w którym zrodził się nowy termin – aplikacja internetowa. Wraz z nim pojawił się szereg nowych problemów, którym jQuery nie mogło już sprostać. Programiści musieli wymyślić nowe rozwiązania, które umożliwiałyby tworzenie tych aplikacji internetowych.

W rezultacie, przez pewien czas nowe rozwiązania wyrastały jak grzyby po deszczu. Nie było miesiąca, aby nie powstał nowy framework, który rozwiązywał część problemów, ale tworzył kolejne. Jednym z lepszych rozwiązań był Angular, który do tej pory jest dość popularny. Ten framework jest kompletnym narzędziem do tworzenia aplikacji internetowych. Angular rozwiązuje wiele problemów, ale jednocześnie narzuca pewien styl pisania aplikacji.

image

Jednocześnie trwał szalony rozwój technologii frontendowych – jednym z pionierów tego rozwoju był Facebook. Szybki wzrost popularności sprawił, że Facebook musiał znaleźć nowe rozwiązania, aby jego rozwijająca się platforma działała sprawnie i szybko. W tym celu stworzył i opublikował bibliotekę o nazwie React, którą poznamy w tym module.

Dlaczego warto znać Reacta?

React jest biblioteką, która rozwiązuje problem prezentowania danych. Dzięki niemu rozległe aplikacje internetowe, jak np. Facebook, zawierające dużą ilość ciągle zmieniających się danych, mogą być poprawnie i szybko wyświetlane w przeglądarce. Nie trzeba się martwić o "ręczną" aktualizację — React wszystko robi za nas.

Co więcej, znajomość Reacta będzie dla Ciebie okazją do dalszego rozwoju znajomości i rozumienia JavaScriptu.

To jednak nie koniec korzyści płynących z używania tej biblioteki! React ma ogromną rzeszę zwolenników, a jego społeczność jest ciągle aktywna. Dzięki temu mamy do dyspozycji bardzo dużo gotowych rozwiązań kompatybilnych z Reactem, a większość pytań, czy wątpliwości, które możesz mieć, ma już swoje odpowiedzi na StackOverflow.

React jest obecnie narzędziem "na czasie". Z jednej strony, sytuacja w każdej chwili może się zmienić, jak pokazuje historia frontendu. Jednak z drugiej strony, React został już zaimplementowany na tak dużej liczbie stron, że na pewno nie zniknie nagle z horyzontu. Poza tym zapewnił on sobie silną pozycję w sercach developerów, a ostatnie ankiety dotyczące JavaScriptu tylko to potwierdzają.

Za popularnością Reacta idzie jeszcze jeden powód, dla którego będziemy się uczyć właśnie tego rozwiązania – ponieważ to bardzo popularna biblioteka, jej znajomość jest wymagana w sporej części ogłoszeń o pracę na stanowisku Junior Web Developera. Jest to więc idealne rozwiązanie na początek Twojej kariery!

Naturalny krok w nauce

Po przeczytaniu tego wstępu możesz mieć wrażenie, że wypływasz teraz na nieznane wody reactowego developmentu i będziemy musieli wszystkiego uczyć się od nowa. Nie masz się jednak o co martwić! O ile React wprowadza wiele nowości, to szybko zauważysz, że jest rozwinięciem dotychczasowej nauki.

Od strony developerskiej, React opiera się o komponenty. Są one definiowane w osobnych plikach .js – każdy z nich poświęcony jednemu komponentowi. To podejście już znamy z poprzedniego projektu!

Nasze komponenty (przeważnie) będą rozszerzeniem bazowych klas dostarczonych przez Reacta. Nie będzie to jednak zupełna nowość – w poprzednim projekcie sami stworzyliśmy bazową klasę widgetu (BaseWidget), z której dziedziczyły (czyli były rozszerzeniem) klasy poszczególnych widgetów.

React pomoże nam jeszcze bardziej uporządkować nasz kod. Jednak na tym nie kończy się jego rola – poza zmianami, które wpłyną na to, jak piszemy kod, będzie też sporo zmian w samym działaniu tego kodu. Do tej pory nasz kod JS trafiał bezpośrednio do przeglądarki – teraz przeglądarka będzie otrzymywać kod stworzony przez Reacta (i inne narzędzia) na podstawie naszych plików.

Dzięki temu nasza aplikacja będzie działać bardzo szybko, a my nie będziemy musieli martwić się o to, w jaki sposób zaktualizować jakiś element na stronie. Co więcej, pisząc kod w najnowszych standardach JS – ES6 i późniejszych – nie musimy martwić się wsparciem w przeglądarkach. Jeśli tak zdecydujemy, nasze środowisko developerskie może skonwertować nasz kod tak, aby działał nawet w Internet Explorerze!

Podobnie jak większość web developerów, nie musisz rozumieć, jak dokładnie Twój kod będzie skonwertowany. Wystarczy, że będziesz rozumieć, że oprócz tego, jak React wpłynie na pisany przez nasz kod, dostarcza on również wiele optymalizacji w samym działaniu kodu w przeglądarce.

Nauka Reacta

Samodzielna nauka Reacta może przyprawić o spory ból głowy. Wielu początkujących developerów przy pierwszym kontakcie z Reactem zderza się z nim jak ze ścianą. Masa tutoriali, które można znaleźć w internecie, opisuje nie tylko samego, czystego Reacta, ale zajmuje się również całym procesem tworzenia środowiska developerskiego, nie wnosząc tak naprawdę niczego na temat samej biblioteki. Często początkujący wpadają w pętlę — żeby używać Reacta, potrzebują narzędzi, a żeby wykorzystać narzędzia, potrzebują Reacta.

Ciężko jest samodzielnie wyjść z takiego impasu, ale wspólnie poradzimy sobie z nim bez problemu. Dostaniesz od nas gotowe środowisko, które będzie świetnym punktem wyjścia. Zaczniemy od uruchomienia tej aplikacji, a następnie będziemy powoli ją rozwijać, poznając funkcjonalności Reacta.

15.2. Pierwsze kroki w React.js

Zaczynamy od razu od uruchomienia podstawowego projektu! Załóż na GitHubie nowe repozytorium, które posłuży nam do nauki Reacta. Następnie sklonuj to repozytorium, a do katalogu projektu rozpakuj paczkę z plikami, pobraną po kliknięciu w poniższy guzik.

Pobierz pliki projektu

Nie zgub ukrytych plików!

Niektóre pliki w paczce mają nazwy zaczynające się od kropki, np. .gitignore czy .babelrc. Zwróć uwagę, czy znalazły się one w katalogu, do którego zostały rozpakowane pliki tego projektu.

Brak tych plików spowoduje błędy, np.:

  • dzięki .gitignore nie wszystkie pliki są wysyłane do repozytorium – bez tego pliku do repozytorium niepotrzebnie trafi m.in. cały katalog node_modules, co niepotrzebnie zwiększy wielokrotnie rozmiar repozytorium i znacznie wydłuży czas wszystkich operacji (np. commitowania czy pushowania),
  • plik .babelrc zawiera konfigurację, która jest odpowiedzialna m.in. za parsowanie kodu JSX, którego będziemy już za chwilę używać – bez niej w terminalu będzie wyświetlał się błąd "Unexpected token", ponieważ aplikacja nie będzie rozpoznawać kodu użytego w tym projekcie.

Dlatego koniecznie sprawdź, czy widzisz w paczce pliki, których nazwy zaczynają się od kropki. Następnie upewnij się, że wszystkie te pliki znajdują się również w katalogu, w którym będziesz realizować projekt.

Uruchomienie projektu

Do uruchomienia projektu wystarczy wykonanie dwóch komend w terminalu:

npm install
npm start

Jak już wiesz, pierwsza z nich zainstaluje wszystkie pakiety npm skonfigurowane w package.json. Druga komenda jest skrótowym zapisem komendy npm run start – czyli uruchamia task start, również zdefiniowany w package.json.

Po uruchomieniu tych komend, w oknie przeglądarki powinna otworzyć się strona z napisem "My first React app". To oznacza, że wszystko zadziałało poprawnie!

Webpack – więcej niż task runner

Zacznijmy od sprawdzenia, jak zadziałała komenda npm start. W pliku package.json możesz zobaczyć, że uruchomione zostało polecenie webpack-dev-server z kilkoma parametrami. Wśród nich nie ma jednak informacji o tym, jaki plik powinien zostać wyświetlony na stronie. Skąd w takim razie ta aplikacja wiedziała, gdzie szukać pliku do wyświetlenia? I dlaczego w package.json mamy tylko dwa taski, zamiast dotychczasowych kilkudziesięciu?

Jest to zaleta używania świetnego narzędzia, którym jest webpack. Przejmuje on nie tylko wszystkie funkcje task runnera, którego używaliśmy dotychczas, ale oprócz tego ma znacznie więcej funkcjonalności! Wyjaśnimy je za chwilę, ale najpierw odpowiemy na pytanie, które z pewnością chodzi Ci już po głowie – dlaczego w takim razie do tej pory używaliśmy tak skomplikowanego task runnera?

Po pierwsze, jak za chwilę zobaczysz, konfiguracja webpacka zapisana jest w plikach JS. Wobec tego nie mogliśmy wprowadzić go, dopóki nie zapoznaliśmy się z JS-em. Drugim powodem jest wartość edukacyjna – w pracy web developera często pojawiają się sytuacje, w których potrzebuje dostosować task runner do własnych potrzeb. Dlatego umiejętność rozumienia działania prostego task runnera była kluczowa. Wreszcie, trzecim argumentem jest kwestia rozmiaru. Webpack jest kombajnem, który świetnie nadaje się do pracy z Reactem, ale do naszych dotychczasowych projektów byłby przerośniętym rozwiązaniem.

Czym jest webpack?

Webpack pełni kilka ról – znacznie więcej niż nasz dotychczasowy task runner. Zacznijmy od taska build – jeśli uruchomisz go za pomocą npm run build, powstanie katalog dist z plikami:

  • index.html – kod HTML z dodanymi odwołaniami do pozostałych wygenerowanych plików,
  • styles_bundle_main.css – wszystkie style naszej aplikacji,
  • oraz scripts_bundle.js – połączony i skonwertowany kod JS naszej aplikacji.

Ten ostatni zawiera nie tylko kod wygenerowany na podstawie naszych plików JS z katalogu src, ale również biblioteki używane w projekcie (w tym React).

Nie radzimy podejmować próby przebrnięcia przez plik dist/scripts_bundle.js – jest on kompletnie nieczytelny. Wynika to z tego, że webpack nie tylko połączył wszystkie niezbędne pliki JS, ale też zminifikował ich kod. Oznacza to, że kod został zoptymalizowany, aby wynikowy plik miał jak najmniejszy rozmiar.

image

To jednak nie wszystko – jak za chwilę się przekonasz, kod, którego użyliśmy w plikach JS, nie jest wspierany przez przeglądarki. Musiał zostać "przetłumaczony" za pomocą biblioteki Babel. Dzięki temu wynikowy kod JS w pliku dist/scripts_bundle.js jest przystosowany do uruchamiania przez przeglądarkę. Tu znów z pomocą przychodzi webpack – to on uruchomi za nas Babela i zadba o to, aby nasz kod JS był przystosowany do starszych przeglądarek.

Wreszcie, webpack jest bardzo popularnym narzędziem, dla którego istnieje spora liczba rozszerzeń. Tak jak do tej pory znajdowaliśmy narzędzie, które za pomocą komendy wykonywaliśmy w naszym task runnerze, tak teraz będziemy stosować rozszerzenia webpacka i dodawać je do konfiguracji.

Jak działa webpack-dev-server?

Po uruchomieniu komendy npm run build możesz zauważyć, że zbudowanie wynikowej strony trwa dłuższą chwilę. Czy to oznacza, że zawsze będziemy musieli tyle czekać, aby zobaczyć efekty zmian w kodzie? Absolutnie nie!

Ułóż okna przeglądarki i edytora kodu tak, aby widzieć oba jednocześnie. W oknie przeglądarki chcesz mieć otwarty podgląd strony, a w edytorze plików otwórz plik src/components/App/App.js. Znajdziesz w nim fragment kodu, który wygląda jak HTML – jest to tag <div>, w którym znajduje się nagłówek <h1>. Pod nagłówkiem <h1> dodaj <h2>Hello world!</h2>, aby ten fragment kodu wyglądał tak:

<div>
  <h1>My first React app</h1>
  <h2>Hello world!</h2>
</div>

Zapisz tak zmieniony plik i zobacz, jak szybko pojawiły się zmiany w oknie przeglądarki!

Szybkość działania webpack-dev-server wynika po części z tego, że nie musi on generować plików wyjściowych. Możesz sprawdzić, że w katalogu projektu nie powstał folder dist (wcześniej wspominaliśmy o jego usunięciu – jeśli u Ciebie nadal istnieje, możesz skasować go teraz). Zamiast tego, strona jest generowana w pamięci aplikacji, co pozwala na błyskawiczną aktualizację podglądu w przeglądarce.

Konfiguracja webpacka

Nie będziemy w tej chwili zgłębiać różnych aspektów konfiguracji webpacka. Warto jednak wiedzieć, gdzie jej szukać. Znajduje się ona w pliku webpack.config.js, umieszczonym bezpośrednio w katalogu projektu. Zacznijmy od poznania ogólnej struktury pliku.

image

Plik zaczyna się od kilku importów. Wyglądają one nieco inaczej – w konfiguracji webpacka używamy require zamiast import, ale łatwo zrozumieć, jaka jest ich rola. Pozwalają one na wykorzystanie pakietów NPM.

Następnie mamy trzy obiekty z fragmentami konfiguracji webpacka. Pozwolą nam one na podział konfiguracji na wersję developerską, służącą do pracy nad projektem, oraz produkcyjną, przeznaczoną do publikacji na serwerze.

Jeśli zajrzysz do pliku package.json, zobaczysz parametr --mode, wykorzystywany w taskach start i build. To właśnie za pomocą tego parametru wybieramy, która konfiguracja zostanie użyta przez webpacka.

Wracając do obiektów znajdujących się w pliku webpack.config.js:

  • obiekt baseConfig zawiera te elementy konfiguracji, które są wspólne dla wersji developerskiej i produkcyjnej,
  • w devConfig znajdują się te fragmenty konfiguracji, które dotyczą wyłącznie wersji developerskiej,
  • analogicznie, w prodConfig znajdziesz tylko fragmenty konfiguracji dotyczące wersji produkcyjnej.

Na końcu pliku znajduje się module.exports, który wyeksportuje konfigurację, aby była dostępna dla webpacka. W naszym przypadku jest to funkcja strzałkowa (wyjaśnienie poniżej), która łączy ze sobą baseConfig oraz jeden z pozostałych obiektów (devConfig lub prodConfig), w zależności od trybu.

Funkcje strzałkowe

To pierwszy moment, w którym spotykamy się z funkcjami strzałkowymi. Będziemy ich używać bardzo często, więc jest to dobry moment na przeczytanie po raz pierwszy informacji dotyczących funkcji strzałkowych. Nie musisz od razu ich ćwiczyć, ani uczyć się czegokolwiek na pamięć – wystarczy, że ogólnie zrozumiesz, czym się różnią od funkcji anonimowych.

Omówmy kilka kluczowych informacji, znajdujących się w konfiguracji webpacka:

  • Właściwość entry wskazuje, w którym pliku JS znajduje się główny kod aplikacji. Wszystkie pozostałe pliki są importowane w tym pliku, albo w plikach importowanych w nim, albo w plikach importowanych przez pliki importowane w głównym pliku itd.
  • Obiekt output wskazuje miejsce, w którym ma być wygenerowana wersja produkcyjna, stworzona za pomocą komendy npm run build. Jest też odpowiedzialny za nazwę pliku, w którym znajdzie się scalony i skonwertowany kod JS naszej aplikacji.
  • Obiekt module zawiera konfigurację dla różnych typów plików – np. dla plików JS czy SCSS.
  • W obiekcie plugins mamy listę wtyczek webpacka, które są niezbędne w naszej aplikacji.

W tym module nie będziemy zmieniać konfiguracji webpacka, ale warto już teraz rzucić na nią okiem i dowiedzieć się, co możemy w niej znaleźć.

Co wyświetla się na stronie?

Przejdźmy teraz do pliku src/index.html – pewnie zwrócisz uwagę na to, że w <body> znajduje się wyłącznie <div id="app"></div>. Z naszego wcześniejszego eksperymentu wiesz już, że nagłówek "Hello World!", który dodaliśmy na stronie, wpisaliśmy w pliku src/components/App/App.js. Jak to się stało, że został wyświetlony na stronie?

Po pierwsze, musisz wiedzieć, że <div id="app"></div> jest wrapperem naszej aplikacji – to w nim React będzie wstawiał całą wygenerowaną zawartość. Jest to skonfigurowane w pliku src/index.js. Otwórz teraz ten plik.

Zobaczysz w nim najpierw kilka importów – działają one bardzo podobnie do importowania, którego używaliśmy w poprzednich modułach. Importujemy tutaj zainstalowane biblioteki react oraz react-dom. Oprócz nich importujemy również komponent App z pliku src/components/App/App.js oraz globalne style naszej aplikacji.

Poza importami, w pliku znajduje się tylko jedna linia kodu, wykorzystująca metodę ReactDom.render. Służy ona do wyświetlenia zawartości strony – w drugim argumencie podajemy element z pliku index.html, do którego będzie wstawiana treść. Ciekawszy jest jednak pierwszy argument...

JSX, czyli JS, który przypomina HTML

Wyrażenie <App /> może Cię dziwić – i słusznie, nigdy wcześniej nie spotkaliśmy się z JSX-em. Pełna nazwa tej składni to JavaScript XML. Pozwala na pisanie kodu, który jest bardzo podobny do HTML-a. W tym wypadku używamy samozamkniętej formy "tagu" App. Równie dobrze moglibyśmy napisać <App></App>, ale wygodniej jest napisać <App />. Dłuższa forma pokazuje jednak, jak bardzo kod JSX jest podobny do HTML-a, którego już znasz.

Oczywiście, w specyfikacji HTML-a nie ma tagu App – jest to nasz własny komponent! Ten sam, który zaimportowaliśmy powyżej, podając ścieżkę ./components/App/App.js! Innymi słowy, ta jedyna (poza importami) linia kodu w index.js mówi: "znajdź element, którego id to 'app', i wstaw do niego komponent App".

Zamykanie komponentów

JSX jest oparty o język XML, stąd też musimy pamiętać, by zawsze zamykać reactowe "tagi". Powyżej pokazaliśmy zamykanie za pomocą pary tagów <App></App> oraz skróconej, samozamkniętej wersji <App />. Kiedy pisząc kod reactowy używasz taga, którego w HTML nie zamykamy, na przykład <img>, pamiętaj, aby go domknąć za pomocą slasha: <img />.

Zobaczmy zatem, jak wygląda ten komponent. Otwórz plik App.js. Znajdziesz w nim – podobnie jak w każdym innym pliku wykorzystującym Reacta – import biblioteki react, a także import stylów tego komponentu. Style omówimy sobie za chwilę – na razie skupmy się na pozostałej zawartości pliku. Znajdziesz w nim klasę App oraz ostatnią linię kodu, która eksportuje tę klasę. Eksportowanie również działa bardzo podobnie do tego, jak wykorzystywaliśmy je do tej pory – nowością jest tylko słowo default. Dzięki niemu, importując App w pliku index.js, możemy pominąć nawiasy klamrowe, czyli nie musimy pisać import {App} from '...';, tak jak do tej pory to robiliśmy.

Zanim wyjaśnimy klasę App, zwróć też uwagę, że w pliku index.js nie musieliśmy tworzyć nowej instancji klasy App za pomocą słowa new, tak jak robiliśmy to w poprzednich modułach. Wyręczył nas w tym JSX, któremu wystarczyło wyrażenie <App /> do stworzenia nowej instancji.

Przejdźmy do wyjaśnienia klasy App. Po pierwsze, dziedziczy ona z klasy React.Component. Ta klasa ma wiele metod, ale w tym momencie jeszcze nie korzystamy z żadnej z nich, więc wrócimy do tego nieco później. Najważniejsze dla nas jest to, że klasa App musi zawierać metodę render, która zostanie wykonana w momencie wstawienia jej na stronie. Od zawartości tej metody zależy to, co wyświetli się w przeglądarce.

Metoda render zwraca obiekt JSX, a konkretniej <div>. Po co użyliśmy takiego elementu, który nie ma żadnej klasy? Mogłoby się wydawać, że jest niepotrzebny – nic bardziej mylnego. Podstawową zasadą tworzenia komponentu jest to, że musi zwracać dokładnie jeden element najwyższego poziomu.

Oznacza to, że nie możemy zwrócić np. <h1> i <h2> – to by były dwa elementy, a musimy zwrócić tylko jeden. Dlatego właśnie w tym przypadku zastosowaliśmy <div>. To on będzie tym jednym elementem – który może mieć w sobie wiele elementów, ale nie może mieć rodzeństwa.

Nie musi to być jednak <div> – równie dobrze może to być sekcja, artykuł, nagłówek czy <main>. Ważne, aby był tylko jeden element najwyższego poziomu, a dopiero w nim możemy umieścić więcej elementów.

Przy okazji warto zauważyć, że <div> wygląda jak zupełnie normalny kod HTML. Podobnie możesz używać też innych elementów znanych z kodu HTML. Pamiętaj jednak, że tak naprawdę, to nie jest kod HTML, tylko JSX. Poza używaniem stworzonych przez nas komponentów jest jeszcze jedna różnica pomiędzy tymi dwiema składniami – zamiast słowa class musimy używać className. Wynika to z faktu, że słowo class jest zarezerwowane w JS do tworzenia klasy (np. class App).

Ćwiczenie

Spróbuj teraz zmienić tag <div> na <main> i dodaj mu klasę app-content. Możesz to zrobić dokładnie tak samo jak w kodzie HTML, z tym że zamiast słowa class użyj className. Po zapisaniu zmiany w pliku zbadaj element wyświetlony na stronie. Jeśli wszystko poszło dobrze, zobaczysz zmianę, którą właśnie wprowadziliśmy.

image

Pozostałe pliki projektu

W projekcie znajdziesz też inne pliki, poza omówionymi powyżej. Dzielą się one na dwie kategorie – konfiguracja i realizacja projektu.

Konfiguracja

Te pliki znajdują się bezpośrednio w katalogu projektu. Są odpowiedzialne za konfigurację pozostałych aspektów projektu, w tym Babela (plik .babelrc).

Realizacja projektu

Zacznijmy od pliku /src/data/dataStore.js – znajdują się w nim dane źródłowe, na podstawie których będziemy wyświetlać komponenty na stronie. Na razie nie musisz do niego zaglądać.

W katalogu src/components znajdziesz wiele katalogów, w których – poza App – nie ma plików .js. Każdy z tych katalogów odpowiada jednemu komponentowi. W miarę realizacji tego modułu, w każdym katalogu stworzymy plik .js, w którym skonfigurujemy dany komponent. Na razie znajdują się w nich tylko pliki .scss, zawierające style każdego z komponentów.

Nie dziw się, że na razie komponent App nie korzysta ze stylów – dopiero za chwilę zajmiemy się ich podłączeniem.

Podsumowanie pierwszego spotkania z Reactem

Pojawiło się dużo nowości, ale jak się nad tym zastanowić, do zrozumienia tego początkowego kodu wykorzystujemy w większości wiedzę zdobytą wcześniej. Nie jest już dla nas nowością dzielenie kodu na mniejsze pliki oraz importowanie i eksportowanie niezbędnych komponentów. Mamy też doświadczenie z używaniem klas oraz ich dziedziczeniem. Nowością może wydawać się JSX, ale przez olbrzymie podobieństwo do kodu HTML, nie będziemy mieć problemu z jego używaniem. Konfiguracja webpacka jest czymś zupełnie nowym, ale będziemy Ci pomagać w jej zmianach – które nie będą zbyt częste po zakończeniu tego modułu.

To oczywiście tylko początek naszej przygody z Reactem – już za chwilę zaczniemy rozwijać ten podstawowy projekt, aby na końcu modułu zbudować pierwszą działającą aplikację! Będzie ona, co prawda, bardzo prosta, ale pozwoli nam lepiej zrozumieć wykorzystanie Reacta.

15.3. Podejście komponentowe

Do tej pory w kursie kilka razy poruszaliśmy już kwestię podejścia komponentowego. To zagadnienie pojawiło się już przy nauce HTML i CSS, gdzie staraliśmy się, aby button był ostylowany tylko raz, niezależnie od tego, ile razy będzie wykorzystany na stronie. Poznając OOP w JS, również wprowadziliśmy podział na komponenty – np. widget wyboru ilości był zastosowany wielokrotnie, ale cała logika jego działania była umieszczona w klasie AmountWidget, która była rozszerzeniem innego komponentu – BaseWidget.

We wcześniejszych modułach podejście komponentowe tłumaczyliśmy najczęściej na przykładzie elementów wykorzystywanych wielokrotnie, ponieważ w ten sposób łatwiej jest zrozumieć sens ich komponentowości. Mieliśmy jednak okazję – również w projekcie pizzerii – stworzyć klasę Cart, która miała tylko jedną instancję. Mimo tego użyliśmy klasy, kierując się czytelnością kodu i standaryzacją podejścia.

React a komponenty

React wprowadzi nas w kolejny poziom podejścia komponentowego – wszystko jest komponentem. Spójrz na nasz (na razie bardzo ubogi) projekt. Mamy w nim komponent App, który z zasady będzie wykorzystany tylko raz, i będzie zajmował się głównie importowaniem innych komponentów. Niemniej jednak jest to też komponent.

Dlatego będziemy się starać tworzyć coraz mniejsze komponenty, składające się z innych komponentów. Każdy z nich będzie odpowiedzialny za siebie oraz poprawne wykorzystanie innych komponentów. Będzie to miało wpływ nie tylko na kod JS.

Style komponentów

Podejście komponentowe wpłynie też bardzo na to, w jaki sposób będziemy pisać style naszej aplikacji. Większość stylów będzie przypisanych do danego komponentu i będzie miała wpływ tylko na niego – poza naturalnym dziedziczeniem stylów takich jak np. color.

Style globalne będą ograniczone do niezbędnego minimum. W trakcie realizacji tego modułu w ogóle nie będziemy ich zmieniać. Dotyczy to wszystkich plików w katalogu src/styles, czyli:

  • settings.scss, w którym znajdują się zmienne (i ew. mixiny),
  • normalize.css, który jest standardem ujednolicenia domyślnych stylów pomiędzy różnymi przeglądarkami,
  • global.scss, zawierający style globalne (np. dla body),
  • pozostałe pliki .scss w tym katalogu mogą zawierać inne globalne style – np. dla typografii, grida, etc.

WAŻNE

Plik settings.scss nie może zawierać żadnych stylów! Mogą znajdować się w nim wyłącznie wyrażenia Sass, które nie renderują stylów. Innymi słowy, możesz umieszczać w tym pliku wyłącznie zmienne i mixiny.

Wynika to z konstrukcji projektu reactowego – kod SCSS nie stanowi jednej całości, a każdy plik .scss danego komponentu jest renderowany osobno. Każdy z nich importuje też plik settings.scss, aby możliwe było używanie zmiennych i mixinów. Wobec tego, gdybyśmy umieścili jakieś style w tym pliku, zostałyby one wstawione wielokrotnie (raz dla każdego komponentu).

Do stylów globalnych, które nie dotyczą konkretnego komponentu, służy plik global.scss oraz pliki importowane w nim (poza settings.scss).

Wykorzystanie stylów w komponentach

React stawia nacisk na podejście komponentowe – do tego stopnia, że ułatwia nam upewnienie się, że style dla jednego komponentu (lub zawartego w nim elementu) nie będą wpływać na inne komponenty na stronie. Dzieje się to za pomocą dynamicznej zmiany klas, zarówno w kodzie JSX, jak i wygenerowanym CSS.

Zobaczmy to na przykładzie, przy okazji podłączając style do komponentu App. Otwórz plik src/components/App/App.js i zacznij od dodania nowego importu (pod importem Reacta).

import styles from './App.scss';

Następnie zmień klasę nadaną elementowi main – zamiast cytatu z tekstem, wartością ma być {styles.component}. Po wykonaniu tych zmian, plik App.js powinien wyglądać tak:

import React from 'react';
import styles from './App.scss';

class App extends React.Component {
  render() {
    return (
      <main className={styles.component}>
        <h1>My first React app</h1>
        <h2>Hello world!</h2>
      </main>
    )
  }
}

export default App;

Zacznijmy od wyjaśnienia nawiasów klamrowych { } – pozwalają one na przełączenie się z kodu JSX na zwykły kod JS. Dzięki temu jako wartość właściwości className ustawiliśmy wartość właściwości component z obiektu styles. Nie przejmuj się, jeśli nie jest to dla Ciebie jeszcze do końca jasne – na razie wystarczy, że zapamiętasz, że nawiasy klamrowe { } pozwalają na wstawienie kodu JS wewnątrz kodu JSX.

Zauważ też, że nie musieliśmy (a wręcz: nie mogliśmy) użyć cudzysłowów wokół nawiasów klamrowych. To bardzo ważne dla poprawnego działania tego kodu!

Nie zapomnij sprawdzić, jak zmienił się wygląd strony w przeglądarce! Już wygląda nieco inaczej, jednak na tym nie koniec – w ten sam sposób dodaj className do elementów <h1> i <h2>, z tym że zamiast component wpisz title dla <h1>, oraz subtitle dla <h2>.

Teraz kod JSX powinien wyglądać następująco:

<main className={styles.component}>
  <h1 className={styles.title}>My first React app</h1>
  <h2 className={styles.subtitle}>Hello world!</h2>
</main>

Teraz strona wygląda dużo lepiej, prawda?

image

Aby zrozumieć, co tak właściwie się wydarzyło, zaczniemy od zbadania obu nagłówków w narzędziach developerskich.

image

Co to za dziwne klasy? Skąd one się wzięły? I dlaczego inspektor mówi, że style znajdują się w <style></style>?

Unikatowe nazwy klas

Zacznijmy od wyjaśnienia nazewnictwa klas. Zaimportowaliśmy wartość stałej styles z pliku App.scss, jednak w tej stałej nie znajdują się style, tylko komponent stworzony przez loadery scss-loader i css-loader. Loadery są wtyczkami webpacka, odpowiedzialnymi za wczytywanie konkretnego typu plików. Ich użycie skonfigurowaliśmy w pliku webpack.config.js w sekcji module obiektu konfiguracyjnego.

Ten komponent, zapisany w styles, posiada właściwości dla każdej klasy użytej w pliku App.scss. Dzięki temu, przypisując właściwości className wartość styles.component sprawiliśmy, że klasą elementu <main> będzie klasa wygenerowana przez css-loader.

Nazwa każdej klasy przypisanej w ten sposób – również skonfigurowana w webpack.config.js – składa się z trzech członów rozdzielonych znakiem podkreślenia _:

  • nazwy komponentu, w tym przypadku App,
  • nazwy klasy użytej w pliku App.scss,
  • pozornie losowym ciągiem 6 znaków (wśród nich może również być podkreślenie _).

Ten ostatni człon zależy od zawartości stylów dla tej klasy (czyli jest to hash, a właściwie jego fragment).

W ten sposób otrzymaliśmy klasę, która na pewno nie powtórzy się nigdzie w kodzie naszej aplikacji. Pozwala nam to na używanie tak ogólnych określeń, jak np. title. Niedługo też przekonasz się, że główne style każdego komponentu przypisaliśmy klasie component. Nie martwimy się powtarzaniem tych samych klas w stylach różnych komponentów, ponieważ będą one zmienione na unikalne klasy.

Zmniejsza to również potrzebę zagnieżdżania stylów w kodzie SCSS. Klasy wykorzystywane w powyższy sposób nie muszą być zagnieżdżone, ponieważ są unikalne. Dzięki temu dużo rzadziej będziemy stosować zagnieżdżanie.

Style w <style>

A dlaczego style znajdują się w znaczniku <style>? Ponieważ wykorzystujemy webpack-dev-server, którego zadaniem jest jak najszybsze wyświetlanie zmian wprowadzanych przez nas w plikach. Ten sposób jest szybszy niż generowanie pliku na dysku i wczytywanie go na stronie.

Nie musisz się tym martwić – w wersji produkcyjnej (po wykonaniu npm run build) wszystkie style znajdą się w pliku styles_bundle_main.css w katalogu dist.

Czy to na pewno dobry pomysł?

Unikalne nazewnictwo klas może wydawać się na początku czymś dziwnym. Możesz pomyśleć, że przez to nie będzie można np. ostylować wszystkich guzików występujących w różnych komponentach – bo przecież każdy będzie miał inną klasę!

Tu właśnie wchodzi komponentowe podejście Reacta – trzeba wtedy guzik potraktować jako osobny komponent, który będzie miał swoje style. Dzięki temu będzie ostylowany tylko raz. Logika zawarta w tym komponencie będzie pozwalała nawet na to, aby ten komponent mógł wyświetlać różne warianty guzika!

Nie przejmuj się tym, że z początku może Ci się to wszystko wydawać niezrozumiałe – bardzo szybko się przyzwyczaisz i zaczniesz doceniać zalety podejścia komponentowego!

Rodzaje komponentów

Jak niedługo się przekonasz, komponenty to nie tylko kod JSX – mogą mieć swoje właściwości (props) oraz przechowywać w pamięci swój stan (state). To nowe pojęcia, ale ich znaczenie nie będzie dla Ciebie nowością.

Przypomnij sobie widget wyboru godziny z poprzedniego modułu. Miał zdefiniowaną wartość minimalną (godzinę otwarcia), maksymalną (godzinę zamknięcia) oraz krok, czyli o jaką najmniejszą wartość da się przesunąć suwak (pół godziny). Teraz te wartości nazwalibyśmy właściwościami (props). Ten widget musiał też pamiętać wartość, którą obecnie wskazuje – np. 18, jeśli został przesunięty na godzinę 18:00 – tę wartość umieścilibyśmy w stanie (state) komponentu.

Jednak nie każdy komponent będzie tak skomplikowany. Niektóre z nich mogą zawierać jedynie elementy HTML oraz ewentualnie ich style. Dla tych prostszych komponentów używa się uproszczonej składni, która nie wymaga tworzenia klasy. Nazywamy je komponentami funkcyjnymi, ponieważ nie są klasą, tylko funkcją. Zobaczmy, jak będzie wyglądała składnia takiego komponentu.

import React from 'react';
import styles from './MyComponent.scss';

const MyComponent = () => (
  <div>
    <h3>Hello world!</h3>
  </div>
);

export default MyComponent;

Porównaj tę składnię z komponentem klasowym, który może wykorzystywać zaawansowane możliwości Reacta.

import React from 'react';
import styles from './MyComponent.scss';

class MyComponent extends React.Component {
  render() {
    return (
      <div>
        <h3>Hello world!</h3>
      </div>
    );
  }
}

export default MyComponent;

Jak widzisz, różnią się tylko tym, co jest dookoła kodu JSX. Jeśli to wszystko, czego będzie potrzebował nasz komponent, możemy śmiało używać komponentu funkcyjnego.

Wiemy, że na tym etapie funkcje strzałkowe są jeszcze dla Ciebie nowością. Pamiętaj jednak, że jest ona bardzo podobna do anonimowej funkcji, której używaliśmy już wielokrotnie. Więcej o funkcjach strzałkowych dowiesz się o niej z naszego poradnika JS. Koniecznie przeczytaj te informacje, ponieważ będziemy dość często wykorzystywać funkcje strzałkowe.

Nie używaj tej składni

Zależy nam, żeby łatwiej było Ci zrozumieć kod komponentu funkcyjnego. Dlatego poniżej znajdziesz niepoprawną składnię. Gdybyśmy jej użyli, zadziałałaby, ale jest to zła praktyka i należy jej unikać.

const MyComponent = function(){
  const content = (
    <div>
      <h3>Hello world!</h3>
    </div>
  );

  return content;
};

Kiedy zamiast funkcji strzałkowej użyliśmy anonimowej funkcji, a kod JSX zapisaliśmy w stałej, powinno Ci być łatwiej zrozumieć działanie takiego komponentu. W wielkim skrócie, zawartość JSX jest zwracana przez funkcję MyComponent, zamiast przez metodę MyComponent.render, jak ma to miejsce w przypadku standardowego komponentu (klasowego).

Zadanie: Tworzenie komponentów

Opowiedzieliśmy Ci trochę o powstaniu Reacta oraz jego zaletach, uruchomiliśmy bazowy projekt, i zaimplementowaliśmy style w komponencie App. Teraz czas na pierwsze zadanie z Reacta! Za chwilę stworzysz dwa komponenty Reacta!

To zadanie składa się z dwóch części:

  1. Stworzenie komponentu klasowego List i wykorzystanie go w komponencie App.
  2. Stworzenie komponentu funkcyjnego Hero i wykorzystanie go w komponencie List.

Nie przejmuj się – to zadanie tylko brzmi groźnie. Za chwilę zobaczysz, że nie będzie ono wielkim wyzwaniem. ;)

Komponent List

Ten komponent będzie prawie identyczny, jak App. Wprowadzimy w nim tylko kilka zmian:

  • będzie znajdował się w pliku src/components/List/List.js, na początek możesz do niego skopiować zawartość z App.js,
  • komponent będzie miał nazwę List – nie zapomnij zmienić wszystkich wystąpień słowa App na List w tym pliku,
  • głównym zwracanym elementem JSX będzie <section>, z taką samą właściwością className, jak <main> w App,
  • wewnątrz sekcji na razie wstaw tylko <h2> z dowolną treścią.

Następnie, w komponencie App użyj nowego komponentu – po nagłówkach (ale wewnątrz tagu <main>) dodaj <List />. Pamiętaj, aby zaimportować ten komponent, podając ścieżkę od pliku App.js do pliku List.js. Będzie trzeba tutaj wykorzystać ścieżkę idącą o jeden poziom do góry, czyli zaczynającą się od ../.

W efekcie tej zmiany, na stronie powinny być teraz widoczne trzy nagłówki – oba wpisane w App, oraz jeden z naszego nowego komponentu List.

image

Komponent Hero

Chcemy, aby tytuł listy miał obrazek w tle. Dlatego stworzymy nowy komponent, który będzie wyświetlał <h2> oraz <img>, zwrappowane w <header>. Nie musisz przejmować się stylami – są już przygotowane. Wystarczy zastosować odpowiednią klasę.

Kilka uwag, które pomogą Ci stworzyć ten komponent:

  • ma znaleźć się w pliku src/components/Hero/Hero.js,
  • będzie to komponent funkcyjny – składnia pliku będzie taka jak podana powyżej (przed zadaniem),
  • głównym elementem w kodzie JSX ma być <header>,
  • wewnątrz headera mają znaleźć się <h2> oraz img,
  • proponowana przez nas zawartość nagłówka <h2> to "Things to do", ale możesz wymyślić inną,
  • jako źródło (src) obrazka możesz podać ilustrację przygotowaną przez nas lub wybrać inny obrazek, np. z Pexels,
  • wszystkie trzy elementy mają mieć atrybut className, którego wartości musisz wpisać samodzielnie, na podstawie pliku stylów tego komponentu – możesz wzorować się na komponencie App, sprawdzając jakich wartości użyliśmy w classNames jego elementów, i jak się one mają do zawartości pliku App.scss.

Po wykonaniu tego komponentu użyj go w komponencie List, zamiast nagłówka <h2>. Jeśli wszystko poszło dobrze, na stronie powinien być widoczny obrazek, a na nim wyśrodkowany nagłówek komponentu List.

image

Co zrobić, jeśli coś nie działa?

Przede wszystkim, przeczytać pierwszy komunikat o błędzie w konsoli i/lub w terminalu, w którym mamy uruchomiony npm start. W tym drugim przypadku zwróć uwagę, że po każdej zmianie w którymkolwiek pliku, wyświetla się nowy zestaw komunikatów. Zaczyna się on od "Compiling...", a kończy na "Compiled successfully." lub "Failed to compile.".

Komunikat o błędzie powinien być wskazówką, co nie działa. Najczęściej będzie to brakujący import (lub posiadający błędną ścieżkę) albo błąd składni. W tym drugim przypadku postaraj się tymczasowo usunąć jak najwięcej swoich zmian – pozostaw podstawową składnię komponentu z przykładów podanych przed tym zadaniem. Zmień w nich tylko wystąpienia MyComponent i sprawdź, czy wtedy komponent działa po wykorzystaniu go w innym komponencie (np. App). Jeśli nie – to pewnie kwestia importów. Natomiast jeśli działa, stopniowo wprowadzaj zmiany w tworzonym komponencie, po każdej z nich sprawdzając, czy zmiana zadziałała.

Po stworzeniu obu komponentów i wyświetleniu Hero na stronie wyślij projekt do Mentora.

15.4. Props – właściwości komponentu

Super, mamy już parę komponentów i nasza aplikacja zaczyna wyglądać coraz lepiej – ale pewnie już domyślasz się, jaki mielibyśmy problem z wykorzystaniem komponentu Hero więcej niż raz. Jego treść jest wpisana w nim samym, więc gdybyśmy wykorzystali go w kilku miejscach, zawsze byłby na nim ten sam tekst!

Rozwiązaniem tego problemu są właściwości komponentu, zwykle określane jako props (skrót od properties). Ich wykorzystanie różni się odrobinę pomiędzy komponentami klasowymi a funkcyjnymi – dlatego tytuł wyświetlany w Hero wpiszemy w komponencie App. Z niego zostanie przekazany do List, a dopiero z niego – do Hero.

Propsy w komponencie klasowym

Zaczynamy od App – otwórz plik i zmień <List /> na:

<List title='Things to do' />

Właśnie w ten sposób ustawia się propsy komponentu. Teraz przechodzimy do komponentu List i zmieniamy <Hero /> na:

<Hero titleText={this.props.title} />

Podobnie, jak wcześniej dla List, tak tutaj ustawiamy właściwość dla Hero. Nazwaliśmy ją nieco inaczej – titleText – tylko po to, aby odróżnić właściwość title przekazywaną do komponentu List, od właściwości titleText przekazywanej do komponentu Hero. W praktyce pewnie obie nazwalibyśmy title, ale na początku nauki lepiej unikać identycznych nazw właściwości, aby nie utrudniać sobie rozumienia kodu.

Jako wartość właściwości titleText podaliśmy nawiasy klamrowe { } – czyli przeszliśmy z kodu JSX do zwykłego JS. W tych nawiasach odwołaliśmy się do obiektu this, który oznacza tę instancję klasy List. W tym obiekcie domyślnie będzie znajdował się obiekt props, zawierający wszystkie właściwości przekazane do tego komponentu. Ostatni człon wyrażenia – title – to nazwa właściwości. Pamiętaj, że nazwę właściwości wymyślamy sami, pamiętając o stosowaniu pisowni camelCase.

Podsumowując – w komponencie klasowym, właściwość title możemy wykorzystać za pomocą wyrażenia this.props.title. Jeśli używamy go w kodzie JSX, umieszczamy go w nawiasach klamrowych { }.

Propsy w komponencie funkcyjnym

Jak widzisz z powyższych fragmentów kodu, propsy ustawiamy tak samo zarówno dla komponentu klasowego, jak i funkcyjnego. Różnią się one jednak sposobem wykorzystania właściwości. W wypadku komponentu funkcyjnego propsy będą przekazane jako argument funkcji. W związku z tym musimy dodać deklarację argumentu props:

const Hero = props => (

Pamiętaj, że w funkcji strzałkowej nie używamy nawiasów okrągłych ( ) okalających listę argumentów, kiedy mamy dokładnie jeden zadeklarowany argument. W przeciwnym wypadku musimy ich używać.

Teraz kiedy w funkcji mamy już do dyspozycji argument props, możemy go wykorzystać:

<h2 className={styles.title}>{props.titleText}</h2>

Różnica, względem wykorzystania props w komponencie klasowym, jest niewielka – po prostu pomijamy człon this. przed props (o ile właśnie tak nazwaliśmy argument funkcji, zadeklarowany w poprzednim fragmencie kodu).

Sprawdzenie działania propsów

Jeśli spojrzysz teraz na podgląd strony, będzie wyglądał dokładnie tak samo, jak wcześniej. Skąd w takim razie mamy wiedzieć, że zmiany, które wprowadziliśmy, na pewno działają? Przejdź do komponentu App i zmień wartość właściwości title przekazywanej do komponentu List w kodzie JSX. Jeśli wszystko działa poprawnie, zmieni się tytuł wyświetlany na stronie!

Przekazywanie propsów – podsumowanie

Powyższe wywody mogły Ci się wydać nieco niejasne – dlaczego przekazaliśmy propsa z App do List, i dopiero z List do Hero? Przekazując propsy, idziemy w dół – przekazujemy je do elementu znajdującego się o jeden poziom niżej. Ponieważ komponent Hero znajduje się wewnątrz elementu List, musimy użyć List jako "pośrednika" w przekazywaniu propsów z nadrzędnego komponentu App. W zrozumieniu powyższego może Ci pomóc ten schemat:

image

Każdy komponent zaznaczyliśmy na nim osobnym kolorem, aby zilustrować, jak "przechodzą" między nimi propsy, oraz na którym etapie props title zamienia się w titleText.

Wartości propsów i dzieci komponentu

W tym momencie musimy Ci przypomnieć, że kod JSX to nie to samo, co kod HTML. Dlatego we właściwościach komponentów możemy przekazywać nie tylko tekst, ale również inne wartości – liczby, tablice, obiekty, etc.

Zastanówmy się nad taką sytuacją: chcemy, aby nasz tytuł listy zawierał fragment kodu HTML – np. <sup>soon!</sup>, który powinien wyświetlić tekst pomniejszony i podniesiony do góry. Najwygodniejszym sposobem rozwiązania tego problemu, będzie zmiana wartości właściwości title na tablicę.

<List title={['Things to do ', <sup>soon!</sup>]} />

Zaczynamy od nawiasów klamrowych { }, ponieważ przechodzimy z kodu JSX do zwykłego JS. W tych nawiasach umieszczamy tablicę, której pierwszym elementem jest tekst, a drugim – kod JSX!

Ta zmiana zadziała, ale spowoduje wyświetlenie ostrzeżenia w konsoli. Wynika to z faktu, że każdy obiekt JSX przekazywany w tablicy musi posiadać właściwość key, która jest unikalna w danej tablicy. W tym wypadku nie zrobi to wielkiej różnicy, ale jest to zasada, której należy trzymać się zawsze. W kolejnym submodule zobaczysz lepszy przykład jej zastosowania – na razie zmieńmy tylko podany powyżej fragment kodu na:

<List title={['Things to do ', <sup key='1'>soon!</sup>]} />

Problem rozwiązany! A co, gdybyśmy chcieli pod nagłówkiem dodać dowolną treść – np. opis listy, zdjęcia, czy dowolne inne komponenty lub kod JSX? W tym wypadku możemy nieco inaczej wykorzystać komponent List. Zamiast formy samozamkniętej, np. <List />, wykorzystamy formę z osobnym zamknięciem – <List></List>. Pomiędzy tymi znacznikami możemy umieścić dowolny kod JS (w nawiasach klamrowych) lub JSX.

<List title={['Things to do ', <sup>soon!</sup>]}>
  <p>I'm planning on doing all these things sooner, rather than later!</p>
</List>

Po tej zmianie jednak akapit nie wyświetli się od razu na stronie. Wynika to z faktu, że cała zawartość znajdująca się pomiędzy tagami komponentu, jest również przekazywana jako props! W tym wypadku będzie to szczególna właściwość o nazwie children. Możemy ją wykorzystać w komponencie List, dodając pod komponentem Hero kod JSX:

<div className={styles.description}>
  {this.props.children}
</div>

Teraz nasz nowy akapit będzie już wyświetlał się na stronie! Co więcej, możesz pozostawić tę zmianę w komponencie List, nawet jeśli zdecydujesz się na usunięcie <p> z elementu <List> w komponencie App. W ten sposób pozostawisz sobie możliwość szybkiego dodania opisu listy (lub innych elementów), jeśli zdecydujesz się na to w przyszłości.

Sprawdzanie typów wartości w propsach

Do tej pory w nauce JS-a rzadko przejmowaliśmy się, czy wartością zmiennej jest np. tekst, czy liczba. W nielicznych sytuacjach, kiedy miało to znaczenie, wybieraliśmy jedno z dwóch podejść. Jeśli wyświetlaliśmy wartość (np. w polu tekstowym <input>), w ogóle nie zwracaliśmy na to uwagi. Jeśli przekazaliśmy liczbę zamiast tekstu, i tak była ona konwertowana na tekst przez przeglądarkę. Drugim rodzajem sytuacji był moment, w którym potrzebowaliśmy wykonywać pewne operacje na jakiejś wartości – wtedy zwykle traktowaliśmy ją jako tekst i konwertowaliśmy na liczbę.

Jak widzisz, nie jest to zbyt jasne – szczególnie gdyby inny developer miał kontynuować pracę nad projektem. W innych językach programowania często można się spotkać z zupełnie innym podejściem – tam każda zmienna wymaga zadeklarowania jej typu. Innymi słowy, już w momencie deklaracji zmiennej (lub stałej, lub argumentu) trzeba zdecydować, czy będzie przechowywać tekst, liczbę czy np. tablicę. JavaScript pozbawiony jest tego aspektu, co staje się przeszkodą w rozbudowanych projektach.

React przychodzi nam jednak z pomocą! Dobrą praktyką jest definiowanie typów dla propsów w każdym komponencie – zarówno klasowym, jak i funkcyjnym.

Instalacja prop-types

Zanim będziemy mogli wdrożyć sprawdzanie typów wartości, musimy zainstalować paczkę prop-types – wykonaj w terminalu komendę:

npm install --save prop-types

Deklaracja typów props

Następnie w pliku List.js zaimportuj ten pakiet:

import PropTypes from 'prop-types';

Definicje typów wpiszemy na samym początku klasy – czyli klasa List będzie się zaczynać następująco:

class List extends React.Component {
  static propTypes = {
    title: PropTypes.node,
    children: PropTypes.node,
  }

  render() {

Omówmy sobie dodany fragment kodu.

  • Słowo kluczowe static, które oznacza, że będziemy definiować statyczną właściwość tej klasy. Oznacza to, że obiekt propTypes nie będzie dostępny jako this.propTypes dla każdej instancji. Będzie za to zapisany jako List.propTypes, czyli właściwość samej klasy, a nie instancji. Właśnie tego oczekuje od nas React, więc zawsze w ten sposób będziemy zapisywać definicje typów właściwości komponentu.
  • Właściwość, w której zapisujemy wspomniane definicje, musi nazywać się propTypes.
  • W tej właściwości zapisujemy obiekt, w którym kluczami są nazwy właściwości komponentów, które mogą być do niego przekazywane.
  • Dla każdej nazwy właściwości podajemy jej typ, wykorzystując typy zapisane w zaimportowanym obiekcie PropTypes.

Zwróć uwagę, że właściwość children traktujemy dokładnie tak samo, jak każdą pozostałą!

PropTypes w komponencie funkcyjnym

Nieco inaczej będzie wyglądała konfiguracja w komponencie funkcyjnym. Otwórz plik Hero.js i dodaj w nim taki sam import, jak powyżej. Następnie pod komponentem Hero (czyli przed eksportem) dodaj:

Hero.propTypes = {
  titleText: PropTypes.node,
};

Jak widzisz, zarówno nazwa właściwości, jak i zawartość obiektu, będą takie same jak w komponencie klasowym. Jest to po prostu inna forma zapisu właściwości propTypes – zadziałałaby również dla komponentu klasowego, ale w nim wolimy mieć definicję typów na początku klasy.

Najczęstsze typy właściwości

Pełną listę typów w pakiecie prop-types znajdziesz w dokumentacji. Do najczęściej wykorzystywanych będą należeć:

  • typy proste, czyli teksty (PropTypes.string), liczby (PropTypes.number) czy wartości logiczne (prawda/fałsz, PropTypes.bool),
  • tablice (PropTypes.array) i obiekty (PropTypes.object),
  • oraz bardzo przydatny typ zbiorczy PropTypes.node, który oznacza "coś, co da się wyświetlić na stronie, lub tablica takich rzeczy".

Powyższe wyjaśnienie tłumaczy, dlaczego w naszym przypadku użyliśmy PropTypes.node – we właściwości title przekazujemy tablicę, zawierającą tekst oraz obiekt JSX. Oba elementy tablicy mogą być wyświetlona na stronie, więc to nie spowoduje błędu.

Wymagane właściwości

Dodatkowo możemy do deklaracji typy dodać .isRequired, jeśli chcemy wyświetlić błąd w konsoli, jeśli komponent nie otrzymał danej właściwości. Jeśli nie dodamy .isRequired, parametr ten będzie opcjonalny. W przypadku komponentów List i Hero tytuł możemy potraktować jako obowiązkowy, więc zmieńmy w nich definicję typu tytułu na PropTypes.node.isRequired.

Domyślna wartość właściwości

Jeszcze jedną ciekawą funkcjonalnością, powiązaną z typami właściwości, jest możliwość ustawienia domyślnych wartości parametrów. Załóżmy, że kiedy nie zostanie podana żadna zawartość opisu listy, chcemy wstawić domyślny opis. W takim razie, w komponencie List, pomiędzy statyczną właściwość propTypes, a metodę render, wstawimy ten kod:

static defaultProps = {
  children: <p>I can do all the things!!!</p>,
}

Przejdź teraz do pliku App.js i usuń zawartość, znajdującą się pomiędzy tagami komponentu List – na stronie powinna wtedy wyświetlić się domyślna zawartość opisu listy!

Dokumentacja komponentu

Zwróć uwagę, że definicja typów propsów (oraz ew. domyślne wartości) tworzą swego rodzaju dokumentację komponentu. Dzięki nim dużo łatwiej będzie w przyszłości rozwijać ten projekt, ponieważ w razie wątpliwości będzie można sprawdzić jakie właściwości przyjmuje ten komponent.

Ten aspekt definicji typów propsów często jest ignorowany, podczas gdy daje bardzo dużo korzyści! Tym bardziej, będzie nam zależało, aby zawsze zaczynać komponent klasowy od deklaracji propTypes.

Zadanie: Wykorzystanie props

Teraz, kiedy znamy już propsy, czas wykorzystać je w praktyce! Z tej okazji wprowadzimy kilka zmian w projekcie:

  1. Z komponentu App do Hero przekazujemy treść nagłówka, ale adres obrazka nadal jest wpisana na stałe w Hero. Zmień to tak, aby ten adres był wpisany w App i przekazywany do List, a z niego do Hero. Pamiętaj, aby dodać sprawdzanie typu tej właściwości – tym razem będzie to tekst. Jeżeli będziesz mieć z tym problem, wróć do schematu w rozdziale "Przekazywanie propsów – podsumowanie", aby przypomnieć sobie, jak propsy przekazywane są między komponentami.
  2. Stwórz nowy komponent klasowy o nazwie Column w pliku src/components/Column/Column.js. Na razie wystarczy, że będzie renderował sekcję, oraz zawierał nagłówek <h3>. Dla obu tych elementów zastosuj odpowiednie classNames, bazując na klasach w Column.scss.
  3. W komponencie List dodaj <div> z className odpowiadającą selektorowi .columns z pliku styli tego komponentu. Do tego diva dodaj trzy kolumny (Column) o różnych tytułach, przekazując je jako właściwości do komponentu Column. Pamiętaj, aby w Column dodać sprawdzanie typu wartości w parametrze!

Wszystkie te operacje wykonywaliśmy już na innych komponentach, więc realizacja zadania nie powinna sprawić Ci dużego problemu. Wzoruj się na innych komponentach, a na pewno praca pójdzie sprawnie!

Rezultatem tego zadania powinno być wyświetlenie trzech kolumn obok siebie. Style dla kolumn były już wcześniej przygotowane, więc przy zastosowaniu właściwych className, powinny bez problemu zadziałać. Upewnij się tylko, że wyświetlasz stronie w oknie szerszym niż 768px – inaczej zastosowane RWD sprawi, że kolumny będą ułożone pionowo.

image

15.5. State – stan komponentu

Nasza aplikacja szybko się rozrasta, ale stosując charakterystyczne dla Reacta podejście komponentowe, nadal łatwo się w niej odnaleźć. Zwróć uwagę, że odkąd poznaliśmy propsy, prawie wszystkie treści wyświetlane na stronie są zdefiniowane w komponencie App. Za chwilę jeszcze bardziej rozwiniemy to podejście – cała zawartość strony będzie zdefiniowana w pliku src/data/dataStore.js, a App będzie te dane przekazywał do właściwych komponentów.

Dzięki temu osiągniemy rozdział pomiędzy treściami (dataStore.js), komponentami (src/components/*/*.js) oraz stylami (src/styles/*.scss oraz src/components/*/*.scss).

Na tym jednak nie koniec – chcemy, aby nasza aplikacja pozwalała na dodawanie kolejnych kolumn do listy, a w kolumnach – dodawanie kart z notatkami (np. rzeczami do zrobienia, książkami do przeczytania, itp.). Aby było to możliwe, nasza aplikacja musi wiedzieć jakie karty aktualnie znajdują się w kolumnach. Nie tylko w momencie uruchomienia aplikacji, kiedy wstawimy karty i kolumny zdefiniowane w dataStore.js, ale również później, po dodaniu kolejnych kolumn czy kart.

W tym przypadku nie wystarczą nam właściwości komponentu, ponieważ z założenia mają one stałą wartość – czyli od momentu stworzenia instancji danego komponentu, props nie powinny się zmieniać.

Tu właśnie pojawia się kolejny fundamentalny aspekt Reacta – state, czyli stan komponentu. To właśnie w nim przechowywane są wartości, które zmieniają się w czasie istnienia danego komponentu. Użyliśmy określenia "w czasie istnienia", ponieważ komponent wcale nie musi istnieć od otwarcia strony do jej zamknięcia. Możesz sobie wyobrazić, że np. nasza aplikacja wyświetlałaby tylko zestawienie tytułów list, a dopiero po kliknięciu byłby dodawany komponent danej listy, ze wszystkimi wykorzystanymi w niej komponentami.

Wykorzystanie danych z dataStore.js

Najpierw jednak chcemy mieć jakąś początkową zawartość strony. Zajmijmy się więc wykorzystaniem danych zawartych w dataStore.js.

Komponent App

Najpierw, w komponencie App importujemy obiekty pageContents i listData:

import {pageContents, listData} from '../../data/dataStore';

Następnie wykorzystamy je w kodzie JSX:

<main className={styles.component}>
  <h1 className={styles.title}>{pageContents.title}</h1>
  <h2 className={styles.subtitle}>{pageContents.subtitle}</h2>
  <List {...listData} />
</main>

Użycie właściwości obiektu pageContents nie powinno już być niespodzianką, ale wyrażenie {...listData} może Cię dziwić. Spotykamy się z nim pierwszy raz, ale szybko się do niego przyzwyczaimy. Jest to spread operator, który pozwala na rozpakowanie obiektu lub tablicy. Oznacza to, że wszystkie właściwości z listData zostaną przypisane do komponentu List, jako jego właściwości.

Bez użycia spread operator musielibyśmy zapisać:

<List
  title={listData.title}
  description={listData.description}
  image={listData.image}
  columns={listData.columns}
/>

A gdyby ten obiekt miał więcej właściwości, ten kod byłby o wiele dłuższy! Przy okazji jednak pozwolił nam na pokazanie, w jaki sposób można zapisać element JSX w wielu liniach. Często będzie to nam pozwalało na znacznie większą przejrzystość kodu!

Komponent List

Przechodząc do komponentu List, musimy wprowadzić kilka zmian. Po pierwsze, zaimportujemy ustawienia z dataStore.js:

import {settings} from '../../data/dataStore';

Następnie w deklaracji typów usuniemy children, a dodamy nowe właściwości:

description: PropTypes.node,
columns: PropTypes.array,

Wreszcie, w domyślnych wartościach propsów usuniemy children, a zamiast niego dodamy:

description: settings.defaultListDescription,

Ostatnia zmiana będzie w kodzie JSX – zamiast {this.props.children} użyjemy {this.props.description}. Jak widzisz, większość zmian wprowadzonych w tym komponencie dotyczy zmiany z użycia propsa children na description. Poza tym zmieniliśmy tylko domyślną wartość opisu, na wartość pobraną z ustawień.

Parsowanie kodu HTML

Jeśli teraz spojrzysz na stronę w przeglądarce, zobaczysz, że wyświetlają nam się fragmenty kodu HTML w tytule listy. No tak, to dlatego, że wcześniej zastosowaliśmy tablicę zawierającą tekst oraz obiekt JSX. Teraz, kiedy pobieramy dane z dataStore.js, nie mamy już takiej możliwości.

Teoretycznie moglibyśmy w dataStore.js stworzyć obiekt JSX, ale – jak się na pewno domyślasz – chcemy pisać aplikację tak, by w przyszłości mogła działać w oparciu o komunikację z API. Wtedy na pewno będziemy otrzymywać tylko tekst (w tym kod HTML), więc zastosujemy inne rozwiązanie.

Zainstaluj pakiet react-html-parser za pomocą komendy:

npm install --save react-html-parser

Następnie w Hero.js zaimportuj go:

import ReactHtmlParser from 'react-html-parser';

Wykorzystamy go do sparsowania kodu HTML w treści nagłówka:

<h2 className={styles.title}>{ReactHtmlParser(props.titleText)}</h2>

Teraz wszystko powinno już działać poprawnie! Zastosuj to samo rozwiązanie dla opisu listy (this.props.description) w komponencie List.

Jeśli wszystko poszło dobrze, jedyne treści wyświetlane na stronie, które nie pochodzą z dataStore.js, to tytuły kolumn. Wstawiliśmy je w poprzednim zadaniu, żeby przetestować czy działa nasz nowy komponent Column. Jak już wspomnieliśmy, kolumny będą też początkowo pobierane z dataStore.js, a następnie będą zapamiętywane w stanie komponentu List.

Stan komponentu

W momencie stworzenia komponentu List musimy nadać mu początkowy stan. W naszym przypadku będzie to lista kolumn, które zawierają karty. Nie musimy ich jednak importować z dataStore.js, ponieważ zrobiliśmy to już w App. Tam, za pomocą spread operator, przekazaliśmy do List wszystkie właściwości z listData, włącznie z tablicą columns. Dlatego początkowy stan listy kolumn będzie korzystał z this.props.columns.

Pojawia się jednak pytanie – gdzie właściwie mamy zdefiniować stan komponentu?

Początkowy stan komponentu

Stan komponentu możemy dodać do niego za pomocą właściwości – podobnie jak metody, dodajemy je bezpośrednio w klasie List. Po tej zmianie początek tej klasy będzie wyglądał następująco:

class List extends React.Component {
  state = {
    columns: this.props.columns || [],
  }

Nowością może być dla Ciebie wykorzystanie operatora lub (||). Jest to częsty zabieg, pozwalający na podanie domyślnej wartości w przypadku, gdy żądana właściwość nie istnieje. Innymi słowy, jeśli this.props.columns nie zostało zdefiniowane, czyli komponent nie otrzymał propsa columns, to w this.state.columns znajdzie się pusta tablica [].

Przy okazji warto zaznaczyć, że tylko i wyłącznie przy ustawianiu początkowego stanu można przypisać wartość do this.state za pomocą znaku równości =. Poza tym przypadkiem zawsze będziemy zmieniać stan za pomocą metody this.setState, odziedziczonej z klasy React.Component.

Wynika to z tego, że w momencie zmiany stanu chcemy ponownie wyrenderować elementy tego komponentu – a metoda this.setState zajmuje się m.in. właśnie tym zadaniem. Wykonuje jednak też inne pożyteczne operacje (z których w tej chwili nie korzystamy), więc zawsze będziemy zmieniać stan za jej pomocą.

Wykorzystanie wartości ze stanu

Teraz kiedy w stanie komponentu List znajduje się już tablica kolumn, jesteśmy gotowi na wyrenderowanie tych kolumn. Pojawi się jednak przed nami pewne wyzwanie. Dobrą praktyką, której należy się trzymać, jest nadawanie klucza (key) każdemu elementowi, który jest elementem tablicy, czy innego zbioru.

Świetnym przykładem są właśnie kolumny – będzie ich wiele, ale w kodzie naszej aplikacji tylko raz użyjemy komponentu Column, ponieważ zrobimy to "w pętli" (a konkretniej, w metodzie .map, która zadziała jak pętla).

W tej sytuacji musimy każdej kolumnie nadać klucz key. Co więcej, musimy to zrobić jawnie – tzn. w kodzie JSX musi być wyrażenie key={}, gdzie w nawiasach będzie jakiś kod JS (np. wartość jakiejś stałej).

Zobaczmy, jak będzie wyglądał ten kod, a za chwilę go sobie omówmy. Zamiast trzech komponentów Column użytych w kodzie JSX metody render, użyj następującego kodu:

{this.state.columns.map(({key, ...columnProps}) => (
  <Column key={key} {...columnProps} />
))}

Metoda .map, którą tutaj wykorzystujemy, jest dostępna dla każdej tablicy (array). Służy ona do modyfikacji każdego jej elementu – ale zamiast zmieniać tablicę, na której została wykonana, zwraca nową tablicę ze zmienionymi wartościami.

Innymi słowy, jest to szybki i wygodny sposób na stworzenie tablicy, której każdy element jest przekonwertowanym elementem tablicy this.state.columns. Owo przekonwertowanie polega na stworzeniu instancji klasy Column za pomocą kodu JSX, wraz z przypisaniem jej właściwości z danego elementu tablicy wejściowej (this.state.columns).

Argumentem metody .map jest funkcja strzałkowa. Metoda .map będzie tej funkcji przekazywać pojedynczy element z tablicy this.state.column. Może być Ci jednak ciężko zrozumieć działanie tej funkcji strzałkowej.

Nie używaj tej składni

Wcześniej już spotkaliśmy się z tym operatorem – pozwalał on na rozpakowanie wszystkich parametrów przekazywanych komponentowi List w pliku App.js. Wyglądał jednak znacznie prościej niż użyty powyżej.

Zapiszmy wykorzystaną powyżej funkcję strzałkową w sposób niezgodny z dobrymi praktykami, ale za to łatwiejszy do zrozumienia.

function(singleColumn){
  const key = singleColumn.key;

  const columnProps = {};

  for(let propName in singleColumn){
    if(propName != 'key'){
      columnProps[propName] = singleColumn[propName];
    }
  }

  return <Column key={key} {...columnProps} />
}

Mamy funkcję, która otrzymuje jeden argument. Z niego zostaje zapisana właściwość key w stałej key, a cała reszta właściwości z argumentu zostaje zapisana w obiekcie columnProps. Na końcu zwracamy obiekt JSX, który posiada właściwość key oraz wszystkie pozostałe.

Wyszła jednak z tego całkiem spora funkcja! Tu z pomocą może nam przyjść spread operator, który bardzo szybko wykona operacje wydzielenia właściwości key oraz zapisania całej reszty właściwości w obiekcie columnProps.

function(singleColumn){
  {key, ...columnProps} = singleColumn;

  return <Column key={key} {...columnProps} />
}

Ten zapis oszczędził nam sporo miejsca! Jednak skoro tylko raz używamy argumentu singleColumn, to możemy w ogóle go nie nazywać, tylko od razu w deklaracji argumentów użyć wyrażenia {key, ...columnProps}.

function({key, ...columnProps}){
  return <Column key={key} {...columnProps} />
}

Kiedy zamienimy powyższy kod na funkcję strzałkową, uzyskamy dokładnie taki zapis, jaki wykorzystaliśmy w metodzie .map powyżej.

Możesz zastanowić się, po co w ogóle ta cała operacja – tzn. po co wydzielamy key, kiedy za chwilę trafia on "w to samo miejsce" co reszta właściwości. Wynika to tylko i wyłącznie ze wspomnianej dobrej praktyki przypisywania klucza key w sposób jawny, czyli np. zapis key={key}.

Nie używaj tej składni

Teoretycznie, równie dobrze mógłby zadziałać następujący kod:

(singleColumn) => {
  return <Column {...singleColumn} />
}

Problem polega na tym, że developer czytający taki kod nie ma pewności, czy klucz key został w ogóle nadany. Co więcej, to by oznaczało, że w danych źródłowym koniecznie musi być unikalna właściwość key – nie może to być np. id, jak w API naszego poprzedniego projektu.

Dlatego pamiętaj – jeśli w pętlu lub metodzie .map generujemy komponent dla każdego elementu z tablicy, musimy jawnie przypisać klucz tego komponentu.

No, ale dość o kluczach – spójrz teraz na podgląd naszej aplikacji w przeglądarce! Zobaczysz w niej trzy kolumny, o tytułach zdefiniowanych w dataStore.js!

Modyfikacja stanu

Oczywiście, cała idea stanu komponentu polega na tym, że można go zmieniać – co właśnie teraz zaimplementujemy. Potrzebujemy tylko jakiegoś powodu do zmiany stanu – dodawanie nowych kolumn świetnie się do tego sprawdzi!

W tym celu będzie nam potrzebny komponent służący do dodawania nowej kolumny – Creator. Jego konstrukcja jest odrobinę bardziej skomplikowana, dlatego przygotowaliśmy go dla Ciebie – razem z wykorzystywanym przez niego komponentem Button. W tym samym pliku znajdziesz też Icon.js, który wykorzystamy nieco później.

Pobierz pliki komponentów

Umieść te pliki we właściwych katalogach – odpowiednio src/components/Creator oraz src/components/Button. Komponent Creator zaimportujemy w pliku List.js i wykorzystamy, dodając do kodu JSX:

<div className={styles.creator}>
  <Creator text={settings.columnCreatorText} action={title => this.addColumn(title)}/>
</div>

Komponent Creator przyjmuje dwie właściwości:

  • text to treść placeholdera w polu tekstowym, która służy wyjaśnieniu, do czego służy dany komponent,
  • action zawiera funkcję, która będzie wykonana w momencie kliknięcia guzika "OK" (widocznego po wpisaniu jakiegoś tekstu w pole tekstowe).

Wyjaśnienie wartości przekazanej w action

W tej właściwości chcemy przekazać do komponentu Creator funkcję, która ma zostać wykonana w przypadku kliknięcia guzika "OK". W naszej klasie za chwilę dodamy metodę addColumn, która ma w tej sytuacji być wykonana.

Aby metoda addColumn działała poprawnie, obiekt this, do którego się w niej odwołujemy musi wskazywać na instancję klasy List. Dlatego nie możemy zapisać action={this.addColumn}. Możesz samodzielnie sprawdzić za pomocą console.log czym byłby wtedy obiekt this w metodzie addColumn.

Istnieją dwa popularne rozwiązania tego problemu. Pierwsze z nich zastosowaliśmy powyżej, wykonując funkcję strzałkową. Jak już zapewne wiesz, funkcja ta nie zmienia znaczenia słowa this, a więc nadal wskazuje ono na instancję tej klasy.

Drugim sposobem jest wykorzystanie metody bind dostępnej dla każdej funkcji. Pozwala ona stworzyć funkcję, która będzie powiązana (bound) z kontekstem podanym jako jej argument. W tym podejściu zapisalibyśmy action={this.addColumn.bind(this)}.

Oba rozwiązania są poprawne i z pewnością spotkasz się z każdym z nich. Wybór jednego z nich jest raczej kwestią konwencji – w naszej ocenie pierwsze podejście jest bardziej przejrzyste i z tego względu je stosujemy.

Zobacz na stronie, jak wygląda ten komponent. Pozornie to tylko pole tekstowe, ale po wpisaniu w nim czegokolwiek, pojawiają się guziki "OK" i "Cancel". Po kliknięciu "OK" wykona się funkcja, przekazana komponentowi Creator w propsie action. Aby dokończyć funkcjonalność dodawania kolumn, musimy więc stworzyć metodę addColumn w klasie List, która będzie zmieniała stan za pomocą metody this.setState. Dodaj poniższy kod przed metodą render:

addColumn(title){
  this.setState(state => (
    {
      columns: [
        ...state.columns,
        {
          key: state.columns.length ? state.columns[state.columns.length-1].key+1 : 0,
          title,
          icon: 'list-alt',
          cards: []
        }
      ]
    }
  ));
}

Ten zapis może wydawać się bardzo skomplikowany, ale oznacza tyle, co "dodaj do this.state.columns nowy obiekt". Argumentem metody this.setState może być obiekt reprezentujący nowy stan (lub jego fragment – wystarczy podać zmieniane właściwości stanu) albo funkcja. Pierwsze rozwiązanie stosujemy, kiedy stan zmieniamy na wartość niezależną od dotychczasowej wartości stanu. Na przykład, jeśli chcielibyśmy w stanie przechowywać wartość pola tekstowego (co robimy w komponencie Creator, jak przekonasz się z opisu w podsumowaniu modułu), to moglibyśmy przekazywać jej obiekt.

W naszym przypadku jednak zmiana będzie zależała od dotychczasowego stanu, ponieważ chcemy dodać nową kolumnę do już istniejących, a nie wstawić nową zamiast nich.

Nie stosuj tej składni

Dla łatwiejszego zrozumienia powyższego kodu zapisaliśmy go w nieco dłuższy sposób i bez wykorzystania funkcji strzałkowych czy spread operator.

addColumn(title){
  this.setState(function(currentState){

    // create new column object with properties
    let newColumn = {
      key: state.columns.length ? state.columns[state.columns.length-1].key+1 : 0,
      title,
      icon: 'list-alt',
      cards: []
    };

    // create copy of current state
    let newState = Array.from(currentState);

    // add new column to new state
    newState.columns.push(newColumn);

    // return new state
    return newState;
  });
}

Nawet jeśli ta składnia jest dla Ciebie dużo bardziej czytelna, nie stosuj jej w swoim kodzie. Przy developowaniu Reacta standardem jest wcześniej zaprezentowana składnia. Stosuj ją, a szybko zaczniesz ją rozumieć.

Wróć do przeglądarki, w polu tekstowym wpisz nazwę nowej kolumny i kliknij "OK". Oprócz trzech domyślnych kolumn powinna pojawić się dodatkowa kolumna, o tytule takim, jaki został wpisany w pole tekstowe!

Dlaczego tak właściwie to zadziałało? Ponieważ zmiana stanu powoduje ponowne renderowanie komponentu! Właśnie dlatego w metodzie render wykorzystujemy stan aplikacji. Nie musimy martwić się o to, czy i kiedy zmienić jakiś element na stronie. Zajmuje się tym React – my tylko zmieniamy stan komponentu.

Nie musisz się też martwić o to, że takie podejście może obciążać przeglądarkę, w kółko renderując te same elementy na stronie. React jest bardzo sprytny w tym zakresie – zmienia tylko to, co wymaga zmiany. Jeśli użyjemy komponentu Creator do stworzenia nowej kolumny, to React doda tylko nową kolumnę w strukturze DOM – mimo że czytając kod komponentu mogłoby się nam wydawać, że usunie i na nowo doda na stronie cały kod komponentu List.

Właśnie takie rozwiązania sprawiają, że React jest wygodny dla Developera, a jednocześnie pozwala na szybkie i sprawne działanie aplikacji, przy możliwie małym obciążeniu przeglądarki. Ale chyba nie ma się co dziwić – w końcu React został stworzony przez zespół Facebooka do usprawnienia działania ich platformy. ;)

Zadanie: Dokończenie aplikacji

Nasza aplikacja jest prawie gotowa – ale jej dokończenie będzie już należało do Ciebie!

W tym momencie mamy komponent List, który renderuje komponenty Column. Posiada również komponent Creator, który pozwala na dodawanie kolejnych kolumn do stanu komponentu List, co powoduje dodanie nowej kolumny na stronie.

Twoim zadaniem jest przeniesienie tej samej logiki "o poziom niżej", czyli celem zadania jest:

  • stworzenie komponentu Card, kierując się strukturą danych w dataStore.js oraz stylami w Card.scss,
  • rozbudowa komponentu Column (wzorując się na List), aby:
    • zapisywał zawartość właściwości cards w stanie komponentu,
    • renderował komponenty Card, w oparciu o swój stan (czyli komponentu Column)
    • posiadał również komponent Creator oraz metodę addCard przekazywaną temu komponentowi, co ma pozwalać na dodawanie kolejnych kart do stanu komponentu Column,
  • wykorzystanie komponentu Icon (zawartego w paczce zip pobranej w tym submodule),
    • w nagłówku <h3> kolumny dodaj <span>, który w Column.scss ma style w klasie .icon,
    • w tym spanie wykorzystaj komponent Icon, z paczki zip pobranej wcześniej w tym submodule,
    • w dataStore.js dla każdej kolumny podana jest właściwość icon, którą należy przekazać do propsa name komponentu Icon.

W efekcie kolumna powinna posiadać karty zdefiniowane w dataStore.js oraz Creator pozwalający na dodawanie kart do tej kolumny.

15.6. Podsumowanie

W tym module było bardzo dużo nowości – poznaliśmy Reacta, wraz z jego podejściem do komponentów! Nauczyliśmy się stosować style dla komponentów, przekazywać właściwości komponentom, oraz korzystać ze stanu komponentu. W ten sposób stworzyliśmy prostą aplikację to-do, którą będziemy rozwijać w następnym module!

Omówienie komponentów Creator i Button

Te komponenty były nam potrzebne do wytłumaczenia zagadnienia stanu komponentu. Jednocześnie, Creator sam wymagał wykorzystania stanu. Dlatego postanowiliśmy dostarczyć Ci gotowy kod tych komponentów, a analizę i zrozumienie ich kodu pozostawić jako samodzielne ćwiczenie. Niemniej jednak poniższy opis powinien znacznie ułatwić Ci to zadanie.

Komponent funkcyjny Button, wykorzystywany w komponencie Creator, służy do wyświetlania guzika. Zawiera on dwa warianty – podstawowy oraz danger, który wywołuje się, przekazując do niego właściwość variant='danger'. Zastosowaliśmy w nim rozwiązanie, które oddziela props variant od pozostałych, które są przekazywane elementowi <button>. Dzięki temu możemy komponentowi Button przekazywać właściwości działające na tym elemencie – np. onClick, który zawiera funkcję wykonywaną w momencie kliknięcia w guzik.

Zastosowaliśmy rozwiązanie dla komponentu Button, które pozwala jednocześnie zastosować kilka wariantów – np. jeden wariant może zmieniać kolorystykę, a inny – rozmiar guzika. Możesz to zaobserwować zmieniając w komponencie Creator props variant dla <Button> np. na variant='danger small regular. Zwróć uwagę, że jeśli dany wariant jest zdefiniowany w stylach komponentu Button, to zostanie zmieniony na unikalną nazwę klasy. W przeciwnym wypadku zostanie potraktowany jako globalny modyfikator, czyli zostanie dodana klasa dokładnie w takim brzmieniu, w jakim została zapisana w propsie variant.

Komponent Creator jest bardziej skomplikowany. Zawiera <input>, którego zmiana powoduje (za pomocą metody handleChange) zmianę stanu komponentu. Dzięki temu wartość pola tekstowego zawsze znajduje się w stanie. Ponadto, jeśli w inpucie znajduje się jakakolwiek zawartość, wyświetlane są guziki – również za pośrednictwem stanu, poprzez nadanie klasy diva.

W momencie kliknięcia guzika "OK" wykonuje się metoda handleOK, która wywołuje funkcję przekazaną w propsie action z argumentem w postaci bieżącej wartości pobranej ze stanu. Oprócz tego, zarówno guzik "OK", jak i "Cancel", usuwają zawartość wpisaną w polu tekstowym, oraz ukrywają guziki.

Dla chętnych

Jak zwykle, przygotowaliśmy kilka zadań dla chętnych!

  1. Przeanalizuj działanie komponentów Creator oraz Button, opisanych powyżej. Następnie dodaj nowy wariant guzika. W komponencie Creator dodaj potwierdzenie przy kliknięciu guzika "Cancel".
  2. W komponencie App dodaj komponent Creator, pozwalający na dodanie nowej listy na stronie.
  3. Uwaga – trudne wyzwanie! Dodaj komponent, który domyślnie będzie wyświetlał tylko guzik menu (tzw. hamburger), a po kliknięciu pokaże rozwijane menu zawierające wszystkie listy i kolumny istniejące obecnie w aplikacji.

Jeśli poznanie Reacta było dla Ciebie wyzwaniem, pierwsze z tych zadań powinno pozwolić Ci nabrać większej wprawy, bez wielkich problemów. Jeżeli zaś ten moduł nie stanowił dla Ciebie problemów, ostatnia z propozycji powinna być ciekawym wyzwaniem.

Powodzenia!

15.7. Quiz powtórkowy

Na koniec tego modułu przygotowaliśmy dla Ciebie quiz powtórkowy. Pomoże Ci on powtórzyć wiedzę z poprzednich modułów.

Odpowiedzi tego quizu nie są nigdzie zapisywane, więc są tylko do Twojej wiadomości. Ten quiz ma Ci posłużyć jako pomoc w nauce – dlatego pod każdym pytaniem znajdziesz guzik, który sprawdzi poprawność Twoich odpowiedzi oraz poda Ci wyjaśnienie zagadnienia poruszanego w tym pytaniu.

1. Metodą możemy nazwać:

Wyjaśnienie

Metodą nazywamy każdą funkcję, która znajduje się w jakimkolwiek obiekcie. Tym obiektem może być klasa, instancja klasy, window, document, console, Math, etc.

Najprościej będzie Ci rozpoznać metodę po tym, że kiedy ją wykonujesz, musisz podać jej nazwę po kropce. Dlaczego w takim razie poprawną odpowiedzią jest również alert? Jak możesz zobaczyć w dokumentacji, w rzeczywistości jest to metoda window.alert, czyli znajdująca się w obiekcie window.

W JavaScripcie możemy odwoływać się do właściwości i metod obiektu window z pominięciem window.. Dlatego możesz nie spodziewać się, że np. alert czy document to w rzeczywistości window.alert i window.document.

2. Element DOM:

Wyjaśnienie

Bardzo często mylone pojęcia to HTML i DOM. Kod HTML jest zawartością pliku *.html. W momencie, gdy ten plik zostanie otworzony przez przeglądarkę, na podstawie kodu HTML zostanie wygenerowane drzewo elementów DOM.

Innymi słowy, kod HTML jest tekstem, a element DOM jest obiektem. Oczywiście, większość kodu HTML zostanie wyświetlona na stronie – ale jeśli jakiś element ma nadany styl display:none, nie zostanie wyświetlony, ale nadal będzie istniał w drzewie DOM.

Drzewem DOM nazywamy zwykle wszystkie elementy, które zostały dodane do dokumentu, czyli są dostępne na stronie. Za pomocą JS możemy jednak tworzyć nowe elementy, które nie zostały jeszcze dodane do drzewa DOM. W związku z tym nie zobaczysz ich w narzędziach developerskich.

Spójrz na ten przykład:

var htmlString = '<strong>Hello World!</strong>';

var domElement = document.createElement('div');
domElement.classList.add('greeting');
domElement.innerHTML = htmlString;

var domElementStrong = domElement.querySelector('strong');
domElementStrong.classList.add('highlighted-text');

Zmienna htmlString zawiera kod HTML, który jest ciągiem znaków. Natomiast zmienna domElement zawiera obiekt, który jest elementem DOM. Istnieje on jednak tylko w tej zmiennej, czyli w oderwaniu (detached) od drzewa DOM. Mimo tego możemy korzystać z jego metod i właściwości.

W momencie przypisania wartości zmiennej htmlString (tekst będący kodem HTML) do właściwości innerHTML tego elementu, kod HTML ze zmiennej htmlString zostanie skonwertowany na obiekty DOM. Dzięki temu możemy odnaleźć element DOM strong i korzystać również z jego właściwości i metod.

Aby stworzony przez nas div, zawierający strong, został dodany na stronie, musimy dodać go jako dziecko jednego z elementów zawartych w drzewie DOM. Na przykład, jeśli zechcemy dodać go bezpośrednio do body, możemy użyć takiego zapisu:

document.body.appendChild(domElement);

Podsumowując – kod HTML jest tekstem, podczas gdy element DOM jest obiektem. Wszystkie elementy wyświetlane na stronie są elementami DOM.

3. Poniżej znajdziesz deklarację funkcji. Chcemy, aby istniała możliwość wykorzystania tej funkcji z jednym, lub z dwoma argumentami. Brakujący warunek w bloku if ma sprawdzać, czy drugi argument został podany. Załóż, że jeśli został podany drugi argument, to będzie liczbą różną od zera.

var playerPoints = {
  alice: 19,
  bob: 17,
  carl: 13,
};

function points(name, newPoints){

  if( /* CONDITION */ ){
    playerPoints[name] += newPoints;
  }

  return playerPoints[name];
}

Wybierz warunki, które spełnią to zadanie, po wstawieniu w miejsce komentarza.

Wyjaśnienie

Omówmy każdą z powyższych odpowiedzi:

  • Warunek typeof(newPoints) != 'undefined' sprawdza, czy wartość argumentu newPoints jest typu innego niż undefined. Taki byłby typ wartości, gdyby funkcja points była wykonana z jednym argumentem (lub bez argumentów). Inną możliwością byłoby, gdyby jako drugi argument została przekazana wartość typu undefined (czyli niezdefiniowana). Ten warunek nie sprawdza, czy wartość drugiego argumentu jest liczbą, ani czy jest różny od zera.
  • Może Cię dziwić warunek newPoints, jednak jest to poprawny zapis. Jeśli użyjemy zmiennej newPoints jako warunku, jej wartość zostanie oceniona jako prawdziwa, poza kilkoma przypadkami – m.in. kiedy jest undefined, liczbą 0, pustym ciągiem znaków '', etc.
  • Operatory porównania >, ==, i != są jak najbardziej poprawne, ale nie spełniają założeń zadania. Warunek newPoints > 0 uwzględni tylko liczby dodatnie (a w treści zadania nie było powiedziane, aby pominąć ujemne), warunek newPoints == 0 będzie spełniony tylko dla zera (a założyliśmy, że zero nigdy nie będzie wartością drugiego argumentu), natomiast warunek newPoints != 0 byłby prawdziwy również, kiedy newPoints będzie undefined (ponieważ undefined jest różne od zera).
  • Warunek newPoints > 0 || newPoints < 0 to kolejne poprawne rozwiązanie – uwzględni wszystkie liczby dodatnie i ujemne, więc będzie fałszywy dla undefined, zera, tekstów, etc.
  • Błędnym rozwiązaniem jest warunek newPoints.length > 1, ponieważ liczby nie posiadają właściwości length. Tę właściwość posiadają tablice oraz teksty.
  • Tablica arguments istnieje w każdej funkcji deklarowanej za pomocą function i zawiera wszystkie argumenty przekazane tej funkcji. Jako że jest tablicą, możemy sprawdzić ilość zawartych w niej elementów za pomocą właściwości length. Jeśli ma więcej niż 1 element, czyli liczba elementów jest równa bądź większa od 2, to znaczy, że podano co najmniej dwa argumenty.

Umiejętność sprawdzenia ilości podanych argumentów jest bardzo ważna, ponieważ pozwala nam na tworzenie bardziej uniwersalnych funkcji. W tym przykładzie funkcja points może być użyta zarówno do sprawdzenia liczby punktów, jak i do dodania (lub odjęcia) punktów jednemu z graczy.

4. W kodzie HTML mamy następujący link:

<a id="link-google" href="https://google.com"></a>

W JavaScripcie zapisaliśmy odwołanie do tego linka w zmiennej googleLink:

var googleLink = document.getElementById('link-google');

Za pomocą JS-a chcemy dodać mu atrybut target="_blank". Której z metod powinniśmy użyć do stworzenia tego atrybutu?

Wyjaśnienie

Ze wszystkich wymienionych powyżej metod, jedynie setTarget została przez nas zmyślona. Wszystkie pozostałe są zaimplementowane w silniku JS i często są bardzo przydatne. Gorąco zachęcamy Cię do zapoznania się z nimi na stronach dokumentacji MDN.

5. W pliku HTML bezpośrednio w <body> znajduje się taki kod:

<header id="page-header" class="wide-header">
  <div class="container">
    <a id="company-name" href="/">Lorem Company</a>
    <a id="menu-trigger" class="button-link" href="#">MENU</a>
  </div>
</header>

Do tej strony podłączony jest poniższy plik z kodem CSS.

body {font-size: 12px;}
body header div a { color: red; }
#page-header .button-link { color: purple; font-size: 14px; }
.wide-header #menu-trigger { color: green; }
a { color: blue; }
[href="/"] { color: orange; }
header { font-size: 16px; }

Zaznacz prawdziwe zdania:

Wyjaśnienie

To zadanie opiera się o zagadnienie specyficzności selektorów (selector specificity). Innymi słowy, chodzi o to, który styl zostanie zastosowany do danego elementu, jeśli pasuje do niego kilka selektorów.

Jeśli mówimy tylko o plikach CSS i pominiemy !important (którego i tak nie powinniśmy używać), to sprawa staje się prosta – wystarczy dla każdego selektora ustalić jego specyficzność (czyli dokładność, czy też "jak dobrze pasuje").

Metaforycznie, chodzi o to, że szybciej zareagujesz, kiedy ktoś na ulicy zawoła Twoje imię, niż kiedy zawoła "Ej ty!". ;)

Zobaczmy jak porównywać specyficzność selektorów:

  1. Zaczynamy od policzenia id (#) występujących w każdym selektorze. Ten, który ma ich najwięcej, jest dokładniejszy od pozostałych, i możemy w tym miejscu zakończyć ich porównywanie.
  2. Następnie liczymy wystąpienia klas (.), selektorów atrybutów ([]), oraz pseudo-klas (:). Ponownie, jeśli któryś ma ich najwięcej, to jest dokładniejszy i wygrywa.
  3. Kolejnym krokiem jest sprawdzenie ilości selektorów elementów (np. div) oraz pseudo-elementów (np. ::before). Jeśli któryś z selektorów ma ich więcej niż pozostałe, to jest dokładniejszy.
  4. Wreszcie, jeśli selektory są sobie równe pod wszystkimi powyższymi kryteriami, liczy się kolejność deklaracji – im niżej w kodzie znajduje się deklaracja, tym jest ważniejsza (czyli nadpisuje wcześniejsze deklaracje).

Przeanalizujmy kwestię koloru tekstu w linku "MENU", zaczynając od najmniej istotnego selektora:

  • selektor a dotyczy wszystkich linków, ale ma tylko jeden selektor elementu,
  • ważniejszy od niego będzie selektor body header div a, który posiada 4 selektory elementów,
  • jeszcze dokładniejszy będzie #page-header .button-link, który zawiera 1 selektor id i 1 selektor klasy,
  • selektor .wide-header #menu-trigger również posiada 1 selektor id i 1 selektor klasy, ale znajduje się niżej w kodzie, i dlatego to właśnie kolor z tej deklaracji zostanie zastosowany.

Zwróć też uwagę, że niektóre właściwości są dziedziczone. W naszym przykładzie ma to miejsce dla rozmiaru czcionki linka "Lorem Company" – żadna deklaracja nie ustawia właściwości font-size temu elementowi, ale header i body mają ustawiony rozmiar czcionki. Jako że header jest wewnątrz body, to wszystkie elementy w header odziedziczą rozmiar czcionki (o ile nie mają ustawionego własnego rozmiaru czcionki). To dziedziczenie nie zakończy się na nich – po nich odziedziczą je elementy zawarte w nich i elementy zawarte w nich.

Spróbuj wstawić powyższy kod HTML i CSS do nowego CodePena, a następnie zbadaj wyświetlone linki. W eksploratorze elementów zobaczysz wszystkie deklaracje, których selektory pasują do tego elementu, jak i właściwości potencjalnie dziedziczone z ich przodków.

15.8. Przygotowanie do egzaminu

W kolejnym module znajdziesz formularz zapisu na egzamin oraz wszystkie informacje na jego temat. Chcielibyśmy jednak już teraz pomóc Ci przygotować się do niego.

Egzamin będzie składał się z pytań testowych wielokrotnego wyboru oraz zadań praktycznych. Do tych pierwszych przygotowały Cię pytania w quizach powtórkowych, więc skupimy się teraz na drugim rodzaju zadań.

Cały egzamin, w tym zadania praktyczne, odbędzie się na platformie rekrutacyjnej, z której często korzystają pracodawcy w trakcie rekrutacji developerów. Tego typu platformy pozwalają na automatyczne sprawdzenie poprawności rozwiązania za pomocą testów jednostkowych.

Na późniejszym etapie kursu nauczymy się tworzyć testy jednostkowe, ale już teraz zaczniesz z nich korzystać, aby przygotować się do egzaminu. Nie przejmuj się – nie będzie to trudne!

Czym są testy jednostkowe?

Do tej pory skupialiśmy się na pisaniu kodu, który jest odpowiedzialny za jakąś funkcjonalność na stronie. Dla zachowania dobrej organizacji kodu porządkowaliśmy go w funkcjach i klasach.

Testy jednostkowe to kod, który sprawdza, czy nasze funkcje i/lub obiekty działają zgodnie z oczekiwaniami.

Weźmy prosty przykład – załóżmy, że w naszym kodzie mamy funkcję calcChange, która oblicza, ile reszty trzeba wydać klientowi, który płaci za zakupy przy kasie. Ta funkcja przyjmuje ona dwa argumenty – należność do zapłacenia oraz kwotę otrzymaną od klienta. Test jednostkowy wywoła tę funkcję i sprawdzi, czy zwracana przez nią wartość jest poprawna.

Korzystając ze zdobytej już przez nas wiedzy, moglibyśmy zapisać to w ten sposób:

const change = calcChange(100, 92.6);
const expectedResult = 7.4;

if(change != expectedResult) {
  console.error('Function calcChange did NOT pass the test!');
}

Ten fragment kodu sprawdza, czy funkcja calcChange zwraca poprawną wartość w tym konkretnym przypadku, jednak wymagałaby od nas samodzielnego uruchamiania i sprawdzania w konsoli czy nie pojawił się błąd.

Dla ułatwienia pracy developerów istnieją różne frameworki i biblioteki, które pozwalają na uruchamianie wielu testów z poziomu terminala. Dzięki temu uruchamianie testów może stać się częścią naszego task runnera.

Jak korzystać z testów jednostkowych?

Poniżej znajdziesz link do pobrania plików z zadaniami treningowymi. W każdym katalogu zadania musisz najpierw zainstalować zależności, uruchamiając komendę npm install, a następnie wykonać testy za pomocą komendy npm run test. Możesz też uruchomić automatyczne wykonywanie testów po każdym zapisie któregokolwiek pliku zadania, za pomocą npm run watch.

Każde z zadań zawiera plik README.md, w którym również znajdziesz instrukcję uruchomienia testów.

W odróżnieniu od pracy w przeglądarce wynik uruchomienia testów (a w związku z tym również naszej aplikacji) będzie widoczny w terminalu. Znajdziesz tam również komunikaty wyświetlane za pomocą console.log. Pokażemy to za chwilę na przykładzie!

Zadania treningowe

Pobierz pliki zadań

Rozpakuj pobraną paczkę z zadaniami do swojego folderu z projektami i przejdź do podkatalogu 1.example. Przeczytaj treść zadania w pliku README.md i otwórz plik src/app.js, w którym znajduje się kod naszej aplikacji.

Jak już wiesz z lektury pliku README.md, Twoim zadaniem będzie naprawienie funkcji sumSeconds, która w niektórych przypadkach działa błędnie. Całe szczęście, nie musisz samodzielnie sprawdzać, w jakich przypadkach tak się dzieje – w tym pomogą nam testy jednostkowe.

Otwórz terminal w podkatalogu 1.example i najpierw uruchom komendę npm install, aby zainstalować niezbędne pakiety. Następnie wykonaj komendę npm run test. Po chwili testy zostaną wykonane i zobaczysz ich wyniki.

Czytanie wyników testów

Zacznijmy od podsumowania:

image

Zacznijmy od Test Suites – są to zestawy testów. W naszym przypadku są to osobne pliki z testami, które nazwaliśmy seconds.test.js, minutes.test.js i hours.test.js. Nie musisz do nich zaglądać – jak łatwo się domyślić, zawierają różne zestawy testów, np. w pliku seconds.test.js sprawdzamy tylko poprawność czasów, których suma jest mniejsza od minuty.

Przejdźmy teraz wyżej i znajdźmy wyniki testów z seconds.test.js:

image

Jak widzisz, przed ścieżką do tego pliku testów znajduje się informacja PASS – oznacza ona, że wszystkie testy zdefiniowane w tym pliku przeszły z wynikiem pozytywnym. Niezależnie od tego, poniżej widzimy wszystkie komunikaty wyświetlone przez console.log.

Przejdź teraz do pliku src/app.js i znajdź console.log, który w nim umieściliśmy. Jak widzisz, przy każdym wykonaniu funkcji sumSeconds wyświetla otrzymany argument, sumę sekund, oraz sformatowany tekst, który zostanie zwrócony przez tę funkcję.

Właśnie te komunikaty widzimy w terminalu, za każdym razem otrzymują też informację o pliku i numerze linii, gdzie został wywołany console.log. Wyświetliły się trzy komunikaty, czyli test z seconds.test.js sprawdzają trzy różne przypadki.

Przejdźmy teraz do wyniku testów z pliku hours.test.js – w tym przypadku przed ścieżką do pliku widzimy FAIL, która mówi nam, że nasza funkcja nie przeszła testów. Poniżej ponownie widzimy wszystkie komunikaty wyświetlone przez console.log.

image

Poniżej wyników z konsoli znajdują się szczegóły każdego testu z pliku hours.test.js, który został oblany przez naszą aplikację. Dla każdego z nich wyświetla się opis testu oraz dwie wartości – poprawna (expected) oraz zwrócona przez testowaną funkcję (received). W pierwszym przypadku widzimy, że oczekiwaną wartością było "1h1m1s", a otrzymana – "61h1m1s", czyli liczba godzin jest błędna.

To powinno być dla nas wskazówką, która pozwoli na znalezienie błędu w kodzie naszej aplikacji, czyli w pliku src/app.js.

Podsumowanie

Zwróć uwagę, że nie musieliśmy w ogóle otwierać plików, znajdujących się w katalogu tests! Nie musisz rozumieć jak zostały napisane testy, ani tym bardziej modyfikować ich w jakikolwiek sposób.

Wszystko, czego potrzebujesz, znajdziesz w terminalu, a jedyny kod wymagający edycji znajduje się w pliku src/app.js.

Oprócz zadania 1.example, w paczce zip znajdziesz też drugie zadanie – 2.styling – tym razem dotyczące kodu SCSS. W nim również znajdziesz opis w pliku README.md i podobnie wymaga ono edycji wyłącznie plików w katalogu src (w tym wypadku: src/style.scss). Pamiętaj, że w katalogu drugiego zadania również musisz uruchomić npm install, zanim uruchomisz testy za pomocą npm run test.

Rozwiązań tych zadań nie musisz nigdzie wysyłać. Są one dodatkowymi materiałami, które przegotowaliśmy dla Ciebie, aby przygotować Cię do egzaminu. Więcej informacji o samym egzaminie znajdziesz w kolejnym module, ale warto już teraz wspomnieć, że w czasie egzaminu będziesz pracować w edytorze online, więc nie będziesz lokalnie uruchamiać projektu.

Aby było Ci łatwiej zapoznać się z platformą egzaminacyjną, pierwszym zadaniem praktycznym na egzaminie będzie zadanie 1.example. Za to zadanie nie będą przyznane żadne punkty, ponieważ służy wyłącznie zapoznaniu się z edytorem online.

Tak więc skup się na razie na realizacji zadań treningowych i nabraniu wprawy w korzystaniu z testów jednostkowych. Powodzenia!

;